您现在的位置是:首页 >其他 >C++ ---- 入门基础知识总结网站首页其他

C++ ---- 入门基础知识总结

Bug程序员小张 2024-06-04 10:27:49
简介C++ ---- 入门基础知识总结

思维导图

目录

命名空间

命名冲突

如何定义命名空间

命名空间定义语法

嵌套定义

同一工程下定义同名命名空间

命名空间的使用

命名空间名称和作用域限定符(: :)

using将命名空间中某个成员“释放”出来

using namespace 命名空间名称

C++标准库命名空间std

C++输入/输出

Hello World的四种写法

自动识别类型

缺省参数

全缺省

半缺省(部分缺省)

缺省值同时出现在声明和定义中

函数重载

构成函数重载的三种场景

参数类型不同

参数个数不同

参数类型顺序不同

缺省和函数重载二义性问题

C++函数修饰规则(linux下测试)

返回值不同不构成函数重载

引用

引用概念

语法特性

应用场景

引用做参数

引用做返回值

常引用

权限的平移(可以)

权限的缩小(可以)

权限的放大(不可以)

 临时变量具有常性

引用和指针的对比

内联函数

概念

内联函数的特性

和宏对比


命名空间

在C/C++中,经常要自定义一些变量、函数在全局或者局部,这些变量名和函数名可能会存在冲突的问题。在C++中提出了命名空间的概念,其作用就是对标识符的名称(变量名、函数名等命名)进行本地化,避免和库中的命名或者其他命名空间中的命名冲突。

命名冲突

下述示例代码中定义了一个malloc全局变量,但是这个命名在库中是个函数的名称,出现了命名冲突问题:

#include <stdio.h>
#include <stdlib.h>

//变量名和库中定义冲突
int malloc = 100;
int main()
{
	printf("%d
",malloc);	
	return 0;
}

 上述命名冲突的问题在C++中可以通过定义命名空间的方式来解决!

namespace zxy
{
	int malloc = 100;
}
int main()
{
	printf("%d",zxy::malloc);	
	return 0;
}

如何定义命名空间

命名空间定义语法

namespace关键字+命名空间的名称+{}就定义好了一个命名空间:

语法:namespace 命名空间名称 {}

下面的代码中定义了两个命名空间zxy1和zxy2, 两个空间中分别定义了a、b两个变量和add函数,但是它们在不同的域中,不存在冲突问题。

namespace zxy1
{
    int a = 985;
    int b = 211;

    int add(int a, int b)
    {
        return a + b;
    }
}
namespace zxy2
{
    int a = 100;
    int b = 200;
    int add(int a, int b)
    {
        return (a + b) * 10;
    }
}

嵌套定义

在命名空间zxy1中嵌套了一个命名空间zxy2:

namespace zxy1
{
	int a = 10;
	namespace zxy2
	{
		int b = 10;
	}
}

同一工程下定义同名命名空间

需要注意的是,当在同一工程下定义了同名的命名空间后,不管定义了多少,编译器最后会把它们合成一个命名空间,虽然你定义了多次,但都是在一个作用域中。这时就要注意命名冲突的问题了。

test1.h

namespace zxy
{
	int a = 10;
	int b = 20;
}

test2.h

namespace zxy
{
	int c = 13;
	int d = 14;
}

在编译器的视角下,它们是这样的:

namespace zxy
{
	int a = 10;
	int b = 20;
	int c = 13;
	int d = 14;
}

1.一个命名空间就定义了一个新的作用域,该空间中的所有内容都局限于该命名空间中。

2.同一工程下同名命名空间最终会被合并成一个。

3.嵌套定义的命名空间,如果想要访问内部的命名空间要先找到其外部的命名空间。

4.关键字namespace 定义命名空间。

命名空间的使用

命名空间名称和作用域限定符(: :

namespace zxy
{
	int a = 10;
	int b = 20;
	int c = 30;

	int add(int x, int y)
	{
		return a + b;
	}
}

int main()
{
	printf("%d
",zxy::a);
	printf("%d
", zxy::add(zxy::b, zxy::c));
	return 0;
}

using将命名空间中某个成员“释放”出来

将某个成员从指定命名空间中引入:

namespace zxy
{
	int a = 10;
	int b = 20;
}

using zxy::a;
int main()
{
	printf("%d
",a);
	return 0;
}

using namespace 命名空间名称

该命名空间中的全部成员被引入:

namespace zxy
{
	int a = 10;
	int b = 20;
	int c = 30;

	int add(int x, int y)
	{
		return a + b;
	}
}

using namespace zxy;
int main()
{
	printf("%d
",a);
	printf("%d
",add(c,b));
	return 0;
}

1.(: :)作用域限定符。

2.using + 命名空间名称 + 作用域限定符 + 命名空间中某个成员(using N: :a),将命名空间中的某个成员引入。

3.using+namespace+命名空间名(using namespace N),将整个命名空间中的内容引入。

4.命名空间名称+作用域限定符+某个成员(N : : x)。

C++标准库命名空间std

初始C++的过程中,常见的一行代码:

using namespace std;

基于上述命名空间的知识,这行代码的作用是将C++标准库命名空间的内容展开,这样做的目的是在日常练习中更加的便捷的使用std空间中的内容。缺点是文章开头提到的命名冲突问题。

C++输入/输出

1.cout是ostream类型的对象,cin是istream类型的对象,endl表示换行,它们都包含在<iostream>头文件中。

2.<<是流插入运算符,>>是流提取运算符。 

3.C++标准库的定义实现都放到了std命名空间中。

Hello World的四种写法

有了上述的描述,现在动手写一个“Hello World”:

#include <iostream>
#include <stdio.h>
//方法2:全部引入
using namespace std;
//方法3:部分引入
using std::cout;
using std::cin;
int main()
{
	//方法1:不引入
	std::cout << "Hello World!" << std::endl;
	//方法2和3写法一样:
	cout << "Hello World!" << endl;
	//方法4:printf(C++兼容C)
	printf("Hello Word!
");
	return 0;
}

上述代码展示了命名空间的几种不同用法,也使用了cout标准输出对象。但是有些场景下printf也有它的一些优势,根据实际情况选择不同的实现方法。 

自动识别类型

#include <iostream>
using namespace std;

int main()
{
	int a;
	double b;
	char ch;
	cin >> a >> b >> ch;

	cout << "int:" << a << endl;
	cout << "double:" << b << endl;
	cout<<"char:" << ch << endl;

	return 0;
}

 自动识别类型的原理

ostream和istream类中分别对<<流插入和>>流提取运算符进行了重载:

缺省参数

全缺省

#include <iostream>
using namespace std;

void Fun(int a = 10, int b = 20)
{
	cout << "a=" << a << ",b=" << b<<endl;
}

int main()
{
	Fun();//不传参,用缺省值
	Fun(100, 200);//传参
	return 0;
}

半缺省(部分缺省)

半缺省给缺省值的时候必须从右往左依次给,不能出现间隔。

错误示例:

void Fun(int a = 10, int b = 30,int c)
{
	cout << "a=" << a << ",b=" << b<<endl;
}
void Fun(int a = 10, int b,int c = 20)
{
	cout << "a=" << a << ",b=" << b<<endl;
}

正确使用

#include <iostream>
using namespace std;

void Fun(int a, int b = 10,int c = 20)
{
	cout << "a=" << a << ",b=" << b<<endl;
}

int main()
{
	Fun(10);
	Fun(100, 200);
	Fun(100, 200,300);
	return 0;
}

在传参的时候是按照顺序穿的,不可以间隔这传参!如:Fun(100, , 200);这是错误的写法。

缺省值同时出现在声明和定义中

在声明和定义中同时出现缺省值是不可以的,如果两个位置给的缺省值不同,编译器就无法确定该用哪个缺省值,所以不支持这样的语法。解决方法就是只在声明中给缺省值

#include <iostream>
using namespace std;

void Fun(int a = 30, int b = 10, int c = 20);

int main()
{
	Fun(10);
	Fun(100, 200);
	Fun(100, 200,300);
	return 0;
}

void Fun(int a, int b, int c)
{
	cout << "a=" << a << ",b=" << b << endl;
}

1.部分缺省的缺省值要从右向左给,不能间隔给缺省值。

2.部分缺省传参的顺序是从左向右传参,不能间隔传参。

3.缺省值不能在声明和定义中同时出现,只在声明中给。

函数重载

C++中允许在同一作用域中出现同名函数,这些同名函数的形参列表必须不同(参数个数不同,参数类型不同,参数顺序不同)。

构成函数重载的三种场景

参数类型不同

//类型不同是不同
int add(int a, int b)
{
	cout << "int:" ;
	return a + b;
}

double add(double c, double d)
{
	cout << "double:" ;
	return c + d;
}

参数个数不同

//个数不同是不同
void fun(int a)
{
	cout << "一个参数"<< endl;
}
void fun(int c, int d)
{
	cout << "两个参数" <<endl;
}

参数类型顺序不同

//参数类型顺序不同
void fun2(int a, char b)
{
	cout << "int ,char" << endl;
}
void fun2(char c, int d)
{
	cout << "char int" << endl;
}

缺省和函数重载二义性问题

下述代码中,无参的fun和两个参数的fun构成了函数重载,但是有参的fun参数给了缺省值,调用的时候以可以fun();调用,当在main函数中调用fun()时,就出现了函数调用不明确的错误。

int fun()
{
	cout << "fun()" << endl;
}
int fun(int a = 10,int b = 20)
{
	cout << "fun(int,int)" << endl;
}
int main()
{
	//二义性,调用不明确
	fun();
	return 0;
}

C++函数修饰规则(linux下测试)

C++支持函数重载是因为C++通过函数修饰规则将同名函数进行了修饰,对于上述参数不同的情况修饰出来的名字不同!

●原理分析

一个程序要运行,需要经历预处理、编译、汇编、链接几个阶段!在链接阶段,当链接器看到有函数调用时,但没有该函数的地址,会去寻找该函数的地址,然后链接到一起。C语言中同名函数没法区分,所以不支持重载。C++对函数名进行了修饰(编译器不同修饰规则可能不同),在链接器的视角来看就是不同的函数名称,所以C++支持函数重载。

●Linux下g++测试

#include <iostream>
using namespace std;

int fun(int a,int b)
{
    return a+b;
}

int fun(int c,double d)
{
    return c;
}

int main()
{
    fun(10,20);
    fun(15,23.4);
    return 0;
}
objdump -S xxxxxx

返回值不同不构成函数重载

按照上述函数名修饰规则支持函数重载的原理,将返回值类型也进行修饰,就可以对同名函数进行区分。但是语法上不支持这样做,原因是在函数调用阶段,没办法指明返回值,函数调用存在二义性。所以返回值不同,不能构成函数重载

int add(int a, int b)
{
	return 10;
}
double add(int c, int d)
{
	return 13.14;
}

int main()
{
	//函数调用阶段无法指明返回值
	add(1,2);
	add(3, 4);
}

1.构成函数重载的三种条件,参数个数不同,参数类型不同,参数类型顺序不同。

2.全缺省和无参同名函数在调用时可能存在调用不明确的问题。

3.返回值不同不能构成函数重载,原因是在函数调用阶段无法指明返回值。

4.C++支持函数重载的原因,是根据函数名修饰规则对同名函数进行修饰,当满足函数重载条件(“三不同”)时,在链接器看来它们就是不同的函数名。

引用

C++中的引用和指针是相互辅佐的关系,有一部分功能是重叠的,各自又有其自身的特点。C++中的引用不能完全的替代指针。

引用概念

引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量开辟空间,它和被引用的变量公用一块内存空间。

●ra引用变量a,在语法层面上没有给引用变量开辟空间。ra和a共用同一块内存空间。

int main()
{
	int a = 100;
	int& ra = a;

	cout <<"&a:" << &a << endl;
	cout <<"&ra:" << &ra << endl;
    return 0;
}

 

 取地址a和取地址ra得到的结果是相同的,说明两者共用同一块空间。

int main()
{
	int a = 100;
	int* ra = &a;

	cout <<"&a:" << &a << endl;
	cout <<"&ra:" << &ra << endl;//取到的是指针的地址
    return 0;
}

 取地址ra取的是指针变量的地址,如果想要取到a的地址,要先对指针变量解引用在取a的地址。

语法特性

引用在定义时必须初始化!

	int a = 10;
	int& ra = a;

	const int& rb = 20;

一个变量可以有多个引用。

	int a = 10;
	int& ra = a;

	int& rra = a;

	int& rrra = rra;

引用一旦引用一个实体,就不能在引用其他实体。

应用场景

引用做参数

传参局部解析(传值调用)

观察上述局部的汇编代码解析图,在add函数调用的过程中,首先先将实参拷贝到寄存中,在压入到函数栈帧。“形参是实参的一份临时拷贝”,通过上图可以更好的理解这句话。

●引用做参数(输出型参数)的优点是减少了一次拷贝,修改形参,实参也跟着修改。这部分功能和指针是重叠的。

void Swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}
int main()
{
	int a = 10,b=20;
	cout << "交换前:a=" << a << ",b=" << b << endl;
	Swap(a,b);
	cout << "交换后:a=" << a << ",b=" << b << endl;
	return 0;
}

引用做返回值

●栈帧销毁,返回值返回过程

如上图所示,在函数返回值返回的过程中,并不是直接返回,而是生成临变量拷贝到寄存器上,如果数据较大也可能保存在上层栈帧中,在间接返回给ret。 

●出了作用域返回对象还在(传引用返回,减少拷贝)

int& Fun2()
{
	static int N = 10;
	N++;
	return N;
}
int main()
{
	int ret2 = Fun2();
	cout << ret2 << endl;
	
    return 0;
}

●出了作用域返回对象不在(传值返回)

int Fun1()
{
    int N = 10;
	N++;
	return N;
}
int main()
{
	int ret1 = Fun1();
	cout << ret1 << endl;

	return 0;
}

当函数返回时,出了函数作用域,如果要返回的对象还在,适用于传引用返回,优点是减少了一次拷贝。如果出了函数作用域对象销毁,则要使用传值返回。

●错误场景

出了函数作用域销毁的局部变量,采用传引用返回:

int& Fun3()
{
	int a = 100;

	return a;
}

int main()
{

	int ra = Fun3();
	return 0;
}

 需要注意的是,当函数调用结束,栈帧销毁后。并不是空间不存在了,而是失去了访问该空间的权利,该空间也不在被保护。

常引用

指针和引用的赋值中,存在权限的放大和缩小问题!!!

权限的平移(可以)

int main()
{
	int a = 10;
	const int b = 20;
	
	int& ra = a;//权限的平移
	const int& rb = b;//权限的平移

	return 0;
}

权限的缩小(可以)

int main()
{
	int a = 10;
	const int& rra = a;//权限的缩小

	return 0;
}

权限的放大(不可以)

int main()
{
	const int b = 20;
	int& rrb = b;
	
	return 0;
}

 临时变量具有常性

如上述的场景中,在类型转换或者传值返回的过程中,会产生临时变量,它们是具有常性的,对它们引用,要用常引用。 

场景1:

int main()
{
	int a = 10;
	double b = 13;

	const int& rb = b;

	return 0;
}

 场景2:

int Fun()
{
	int n = 10;
	return n;
}

int main()
{
	const int& rn = Fun();
	return 0;
}

引用和指针的对比

1.引用在底层上实际上是有空间的,引用是按照指针方式实现的。在语法层认为,引用是一个别名,没有独立空间,和引用实体共用同一块空间。

2.引用和指针是互补的,各有优点。

3.引用在概念上是一个变量的别名不需要额外的空间(语法层面上),指针要存储一个变量的地址。

4.引用在定义时必须初始化,指针没有要求。

5.引用很专一,引用了一个实体后就不能在引用其它实体。指针的指向随意改变。这也是引用无法代替实体的原因只一,不能改变指向,诸如链表的增删查改就无法得到很好的实现。

6.没有空引用,但有空指针。

7.在sizeof中的含义不同,sizeof(引用)计算的是引用实体的类型大小,指针不管指向的是什么类型的数据,计算的都是地址空间所占字节的大小,32位平台占4字节,64位平台下占8字节。

8.引用++和--是引用的实体改变,指针++和--是向后或者向前偏移一个类型的大小。

9.有多级指针,没有多级引用,引用一个别名,实际上和引用实体是一样的。

10.访问实体的方式不同,指针需要解引用,引用编译器自己处理,用户直接使用即可。

11.引用比指针更加的安全。

内联函数

概念

关键字inline修饰的函数叫做内联函数,编译器会在编译的预处理阶段将内联函数展开,没有函数调用建立栈帧的消耗,内联函数提升程序运行的效率。

●正常函数调用

int add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10, b = 20;
	int ret = add(a, b);

	cout << ret << endl;
	return 0;
}

 上述汇编代码是正常函数调用,call add跳转到目标函数。

●内联函数调用

在debug模式下,编译器默认不会展开inline函数,需要对编译器进行设置。

inline int add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10, b = 20;
	int ret = add(a, b);

	cout << ret << endl;
	return 0;
}

 通过对汇编代码的观察可以发现,内联函数确实被展开了。

内联函数的特性

1.inline是以空间换时间的做法,如果将编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺点:最后形成的目标文件会变大。优点:少了函数调用创建栈帧的开销,提高程序的运行效率。

2.函数前加了inline关键字,只是对编译器的建议,编译器可以忽略。一些比较成的函数编译器不会将其展开。

inline int add(int a, int b)
{
	int c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	c = a + b;
	return c;
}

int main()
{
	int a = 10, b = 20;
	int sum = 0;
	sum = add(a,b);
	return 0;
}

 如上述测试函数,进行了inline声明。但是最终是否展开取决于编译器,较长的函数,编译器会忽略内联的请求。

3.内联适用于代码规模小,频繁调用的函数。

4.内联函数的声明和定义分离会导致报错,原因是内联函数函数名不进入符号表,在链接的过程中,找不到函数地址。

add.cpp

inline int add(int a, int b)
{
	int c = a + b;
	return c;
}

add.h

#pragma once
int add(int a, int b);

test.cpp

#include <iostream>

using namespace std;
#include "add.h"

int main()
{
	int a = 10, b = 20;
	int sum = 0;
	sum = add(a,b);
	
	cout << sum << endl;

	return 0;
}

 

链接错误: 

和宏对比

●替代宏的技术

1.常量的定义:const或者枚举enum

2.短小函数的定义,用内联函数

●宏和内联函数对比

1.代码长度

宏在预处理阶段进行替换,内联函数在编译阶段展开。都会使程序的长度变长。

2.执行速度

宏和内联函数都没有函数栈帧创建和销毁的消耗,执行速度比函数调用快。

3.参数类型

宏的参数与类型无关,不进行类型检测,所以宏的使用不够严谨。内联函数的使用和常规函数相同,参数与类型是有关的。

4.调试

宏不支持调试,内联函数支持调试。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。