c++关键字
在C语言98版本中,一共有32个关键字,在C++98版本中,一共有63个关键字,注意:在C++11版本中,并不是只有63个关键字,在说明有多少个关键字时,应该给出在什么版本中有多少个关键字。
命名空间
命名空间就是一个作用域,例如一个班级,一个学校都是一个命名空间;
主要作用为避免命名冲突
如何定义一个命名空间?namespace N1{变量,函数}
- 作用域限定符::
命名空间中的成员如何使用?
(1)可以用来访问全局变量,直接在成员前加命名空间的名字与作用域限定符,表示访问该命名空间作用域中的成员;
(2)也可以使用形如using N1::a 这样的来访问,但是此种方法若有类似的同名全局作用域,会发生冲突,
(3)可以使用using namespace N1类似的方法。如果工程存在相同的命名空间,编译器会将命名空间合并成一份
缺省参数
缺省参数就是在定义函数时,给函数的参数带上一个默认值,在使用时可以传参,也可以不用传参,若传参,使用用户传递的参数,若用户没有传递参数,则使用默认值;
分为全缺省参数和半缺省参数
- 全缺省参数:即左右的参数都带有默认值,按照从左到右的顺序匹配
- 半缺省参数:部分参数带有默认值,只能从右到左给出,不能间隔
注意:缺省参数不能在函数的声明和定义同时给出,一般在声明时给出,因为有时候需要用到第三方库,要给出函数声明才能使用,而且如果定义的参数的初值不一样,编译器就会混乱;缺省参数值只能是常量或全局变量
函数重载
必须在相同的作用域,相同名字的函数,如果参数列表不同,与返回值类型无关,才能是函数重载,C语言不支持函数重载,因为C语言函数名字的修饰规则是编译器在函数名字前加(vs编译器),在C++中,函数名字修饰规则是将函数的名字和参数的类型结合在一起_,因此,如果C语言中存在函数重载,几个函数在底层所用的名字相同,会显示重定义,而在C++中,则不会。若要C语言和C++的工程链接在一起,需要使用extern"c",让编译器将这个函数按照c语言的风格进行编译。
例如:
void TestFunc(int a = 10)
{
cout << "void TestFunc(int)" << endl;
}
void TestFunc(int a)
{
cout << "void TestFunc(int)" << endl;
}
int main()
{
return 0;
}
这两个函数无法形成重载,因为参数列表要不同。
函数重载调用准则:
1.默认参数类型能匹配
2.默认类型转换能匹配
注意:可调函数不唯一,存在二义性,报错;无可调函数,报错
例如:
void F(int a)
{}
int F(int a)
{}
错误,返回值不是判断标准,参数完全相同,函数名字相同,报错
void F(int a)
{}
int F(int a, int)
{}
构成函数重载,不报错,例如F(1),调用void (int a);F(1,2),调用int F(int a,int);
void F(int a, int c = 0)
{}
void F(int a, int)
{}
不满足函数重载标准,参数列表应该不同,无法构成重载
void F(int a)
{}
void F(int a, int c = 0)
{}
构成函数重载,但是调用存在二义性
源代码(github):
https://github.com/wangbiy/C-/tree/master/test_2019_7_23_2/test_2019_7_23_2
引用
- 概念:即给已存在变量起一个别名,具体形式是类型&引用变量名(对象名)=引用实体
- 注意: 引用类型必须和引用实体是同种类型的;引用在定义时必须初始化;一个变量可以有多个引用;引用一旦引用一个实体,就不能再引用其他实体,引用与实体相比,实体的生命周期长,例如:
int main()
{
int a = 10;
int&ra = a;
int&rra = ra;
cout << &ra << "=" << &a <<"="<<&rra<< endl;
ra = 20;
cout << ra << a << rra << endl;
system("pause");
return 0;
}
分析:第一次输出三个的地址是相同的,后边ra的值发生改变,另外两个随之也发生改变。
int main()
{
int a = 10;
int&ra = a;
int b = 20;
ra = b;//赋值,不是引用
cout << &ra << " " << &b << endl;//不是一个地址
system("pause");
return 0;
}
分析:用ra引用了a,之后ra=b,并不是引用,因为不能再引用其他实体了,只是赋值。
- 常引用:即const类型的引用,
1、例如:
const int a=10;
int& ra=a;
分析:这样是不可以的,因为在C++中,用const修饰,这时a是常量,不能改变,用普通的类型来引用,说明a可以改变,因此应为:
const int a=10;
const int& ra=a;
2、例如:
double d = 12.34;
const int&rd = d;//创建了临时变量,将12放进了临时变量空间中,rd引用了临时变量,
//rd不知道临时变量的名字和地址,所以这个空间是不能修改,所以加const
cout << &rd << " " << &d << endl;
分析:这时输出的rd的地址和d的地址不同,并且rd在引用d时应该用const修饰,因为创建了临时变量,引用的类型是int类型,即将12放进了临时空间,rd引用了临时变量,又因为rd不知道临时变量的名字和地址,因此这个空间不能修改,因此应该加上const.
- 使用场景:
1、做参数,例如
void Swap(int& ra, int& rb)
{
int t = rb;
rb = ra;
ra = t;
}
它比指针安全,因为指针在使用前要进行判空,而引用不需要
2、返回值类型,不要返回栈上的空间,引用类型作为返回值,返回的变量的生命周期要比函数长,例如:
int& Add(int left, int right)
{
int ret = left + right;
return ret;
}
int main()
{
int a = 10;
int b = 20;
int& retValue = Add(a, b);
printf("%d", retValue);
Add(100, 200);//这时retValue会变成300,因为retValue实际上引用的是函数运行的空间,是这块空间的别名,
//Add函数在返回后,操作系统会回收函数运行的空间,但没有销毁,会留下垃圾数据,所以retValue会接收这个数据30,之后再次调用这个函数,又会给这个函数分配这个空间,又因为retValue引用的这个空间,所以最终retValue会变成300
printf("%d", retValue);//虽然打印的是300,但其实只要运行一次retValue会改变,不安全,但如果不是引用,那么函数在返回后,回收空间,第二次调用这个函数时,这个空间已经将上一次的数据处理掉了
system("pause");
return 0;
}
分析:第一次打印的retValue的值是30,而在进行函数第二次调用后,retValue并没有接收,但是retValue的值变为300,因为retValue实际上引用的是函数运行的空间,是这块空间的别名,函数一旦返回,操作系统会回收函数运行的空间,但并不是销毁,会留下垃圾数据,因此retValue会接收第一次调用的数据,第二次调用函数时,又会给函数分配这个空间,retValue引用的是这个空间,函数返回,遗留数据是300,retValue变为300,但是在第二次打印retValue时,即使打印的是300,但只要每次程序重新运行,它的值就会发生改变,不安全。
如果没有引用,第二次调用函数时,这时分配的空间已经将上一次的数据处理掉了,两次打印都是30,retValue只是记录第一次调用遗留的数据。
- 引用与指针的不同点与相同点
相同点:
引用在底层是按照指针的方式进行处理的,T&相当于T* const,const T &相当于const T* const,在概念层面,引用变量只是起个别名,没有实际空间,与其实体共用一块内存,而在底层,引用就是指针,实际上是有空间的,两者效率一致。
不同点:
1、本质上:引用是别名,指针是地址;
(1)引用在定义时必须初始化,指针不需要;
(2)引用在定义时引用一个实体后,就不能再引用其他实体了,而指针可以在任何时候指向一个同类型实体,例如:
string str1="a";
string str2="b";
string& str3=str1;
str3=str2;
但是不能
&str3=str2;
会显示表达式必须是可修改的左值;
(3)没有NULL引用,但是有NULL指针
(4)在sizeof中含义不同,引用结果是引用类型的大小,指针结果是地址空间所占字节大小;
(5)引用自加是引用实体增加1,指针自加是指针向后偏移一个类型的大小;
(6)有多级指针,但是没有多级引用;
(7)访问实体方式不同,指针需要显式解引用,引用是由编译器自己解决;
(8)指针在使用时要判空,引用不需要;
替代C语言中宏变量和宏函数
- 宏变量:因为宏变量中变量不会进行类型检测,为了解决这个问题,C++中,运用const来进行替代,例如:
const int a = 10;
int arr[a];
分析:实际上这里的a就是10,编译器在遇到a时就会替换为10
例如:
const int a = 10;
int* pa = (int*)&a;
*pa = 100;
cout << a << endl;
cout << *pa <<endl;
分析:虽然打印的结果是10和100,即在编译阶段显示已经将const修饰的a替换成为10,但是在使用变量名输出时,编译器会出现一种类似于宏定义的功能一样的行为,将变量名替换为初值,可见,const修饰的局部变量并不能做到真正的不变,而是编译器对其进行了一些优化行为,也就会造成*pa是100,但此时,虽然a打印出来的是10,但其实这时a空间的值是100,如图:
解决这种问题的方法是volatile关键字,它使得程序每次直接去内存中读取变量值而不是读寄存器值,这个作用在解决一些不是程序而是由于别的原因修改了变量值时非常有用,它防止编译器过度优化,例如此时加上volatile关键字
volatile const int a = 10;//volatile的作用是防止编译器过度优化
int* pa = (int*)&a;
*pa = 100;
cout << a << endl;//此时打印出来的值为100,编译器没有执行类似于宏定义
此时就不会出现过度优化的问题;
- 宏函数(在预处理阶段进行替换)
例如:
#define Max(a,b) (((a)>(b))? (a):(b))
int main()
{
int a = 10;
int b = 20;
cout << Max(a, b)<< endl;//20
cout << Max(++b, a)<< endl;//22
system("pause");
return 0;
}
分析:这时候第二次我们想要的是21,但最终结果是22,因为在替换时先++b,然后打印又++b,因为宏后面a和b带有括号
为了避免这个缺陷,引入了内联函数,即inline关键字修饰的函数
内联函数
例如:
inline int Max(int left, int right)
{
return left > right ? left:right;
}
int main()
{
int a = 10;
int b = 20;
cout << Max(a, b) << endl;//20
cout << Max(++b, a) << endl;//21
system("pause");
return 0;
}
分析:内联函数是在编译阶段替换,没有副作用,也没有函数调用(压栈)的开销,代码运行效率高
注意:inline修饰的函数只是一个建议,是以空间换时间的方法,若代码比较长或者有循环或者递归时,是不适宜使用内联函数的,在使用内联函数时,不能将声明和定义分开,因为inline修饰的函数只作用于当前文件。
宏的优点和缺点
-
优点:
1.增强代码的复用性。
使用宏定义可以用宏代替在程序中经常使用的常量,当常量发生改变时,不用对整个程序中的该常量进行修改,只需要修改宏定义的字符串
2.提高性能。
使用带参数的宏定义可以实现函数调用的功能,减少系统开销,提高性能 -
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
例如没加括号时带来的边界问题,例如
#define Mul(a,b) a*b
int a=1;
int b=2;
int c=3;
int sum=0;
sum=Mul(a+b,c);
我们想要的结果是9,但最终程序的结果是7,因为它展开是sum=a+b*c,而不是(a+b)*c,因此会出现问题,那么如何避免这种问题,只需要在进行宏定义时加上括号就行。但在一些特殊情况,即使加了括号也无济于事,即在宏定义时出现–或者++这样的操作时,也无法避免问题,例如:
#define MAX(a,b) ((a)>(b) ? (a):(b)
int a=3;
int b=1;
int max=0;
max=MAX(a++,b);
经过预编译中替换,就成为了max=((a++)>(b) ?(a++):(b)),计算出的结果是5,而不是我们想要的4.
3.没有类型安全的检查 。
相当于const修饰变量来讲,宏定义的变量,系统在进行预编译时,将其全部替换,不会进行类型安全的检查,而使用const修饰的变量,是在编译时期进行替换,一旦发生类型不安全时,就会报错。
auto关键字
在C++11中,将auto关键字作为类型指示符,用于声明变量,在编译阶段进行推演,我们不用自己去想是什么类型,而是编译器将auto替换成变量实际类型
- auto与指针
和指针结合其后加不加*是没有区别的 - auto与引用
auto与引用结合必须加& - 在同一行定义多个变量类型应该相同
例如:
auto a=1,b=2;
不能是
auto a=1,b=2.14;
- auto不能推导的场景
auto不能用作函数的参数,因为编译器无法将auto定义的参数推演;它也不能声明数组 - 基于范围的for循环
例如:
int array[] = { 1, 2, 3, 4, 5 };
int e = 0;
for (auto&e : array)
e *= 2;
for (auto e:array)
cout << e << " ";
结果是2 4 6 8 10,使用引用即对数组中每个元素的引用,因为要修改数组的元素,可以实现数组的for循环遍历,拿到数组的每个元素,非常方便
范围for的使用条件:范围必须是确定的,例如:
void test(int arr[10]){}
for(auto e:arr)
这是错误的,这时arr已经退化成指针,不知道要打印几个元素。
指针空值nullptr关键字(c++11)
我们知道平常所用的空值是NULL,在传统的C头文件(stddef.h)中,NULL可能被定义为0或者无类型指针(void*)的常量,此时就会出现问题,例如:
void test(int a)
{
cout << "test(int)" << endl;
}
void test(int* a)
{
cout << "test(int*)" << endl;
}
int main()
{
test(0);
test(NULL);
//这两个都是调用的第一个函数,因为这时将NULL当做宏定义,即#define NULL 0
//应当用test(nullptr)
test(nullptr);
system("pause");
return 0;
}
程序本意上是通过test(NULL)调用指针版本的test(int*)函数,但是由于将NULL被定义为0或者(void*)类型的函数,与程序要求的相悖,而test(0)函数实际上是按照0编译的,如果要让其按照指针方式来使用,必须对其进行强转(void*)0,为了考虑兼容性,C++11给出了全新的nullptr表示空值指针,它代表一个指针空值常量,是有类型的,类型为nullptr_t,注意:nullptr为关键字,而nullptr_t是一种类型;在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
源代码(github):
https://github.com/wangbiy/C-/tree/master/test_2019_7_23_3/test_2019_7_23_3