1.C++关键字
asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast
2.命名空间
对于c++来说,变量,函数,类的名称都存在于全局作用域中,可能会发生冲突。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。
同一个作用域中只能定义一个同名的变量。
例如:
namespace yj
{
int a;
int b;
int Add(int left,int right)
{
return left + right;
}
}
命名空间有以下几种特点:
1.命名空间中的内容既可以定义变量,也可以定义函数。
2.命名空间可以嵌套使用。
3.同一个工程中可以存在多个相同名称的空间,编译器最后会合成在同一个命名空间之内。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
//1.使用前加上命名空间和作用域限定符
int main()
{
printf("%d\n",N::a);
return 0;
}
//2.使用using 将命名空间中的成员导入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
//3.使用using namespace 命名空间名称导入
using namespace N;
int main()
{
Add(10,20);
return 0;
}
3.C++输入与输出
#include<iostream>
using namespace std;
int main()
{
int a;
cin >> a ;
cout << a << endl;
cout<<"hello world"<<end1;
system("pause");
return 0;
}
说明
1.使用cout标准输出的cin标准输入时,必须包含头文件以及std标准命名空间。
2.使用C++输入输出更加方便,比如:整形 — %d,字符 — %c
4.缺省参数
概念
缺省参数是声明或定义函数时为函数的参数制定一个默认值。在调用该函数时,如果没有指定实参,则采用该默认值,否则使用指定的实参。
缺省参数分类:
1.全缺省参数
void TestFunc(int a = 0)
{
cout<<a<<end1;
}
int main()
{
TestFunc();//没有传参时,使用参数的默认值;
TestFunc(10);//传参时,使用指定的实参;
}
2.半缺省参数
void TestFunc(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给 ;
- 缺省参数不能在函数声明和定义中同时出现;
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个 缺省值。
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
5.函数重载
概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似,数据不同的问题。
int Add (int left,int right)
{
return left + right;
}
double Add(double left,double right)
{
return left + right;
}
long Add(long left,long right)
{
return left + right;
}
int main()
{
Add(10,20);
Add(10.0,20.0);
Add(10L,20L);
system("pause");
return 0;
}
extern"c"
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern"c",意思是告诉编译器,将该函数按照C语言的规则来编译。然后可以用c语言来调用它。
extern "c" int Add(int left,int right)
int main()
{
Add(1,2);
return 0;
}
代码编译的过程:
F.h
F.cpp
main.cpp
1.预处理 --》展开头文件,宏替换,条件编译,去掉注释。。。
F.i ---- main.i
2.编译 --》检查语法,生成汇编代码
F.s ---- main.s
3.汇编 --》汇编代码生成二进制的机器码
F.o ---- main.o
4.链接 --》生成可执行的程序
a.out
代码编译的时候主要有两种错误:编译错误和链接错误。
编译错误:写代码不规范,语法错误
链接错误:找不到目标函数的文件
函数名的修饰规则:
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包 含在终的名字中,就可保证名字在底层的全局唯一性。
6.引用
概念
引用不是定义一个新的变量,而是给已经存在的变量取了一个别名,编译器不会引用变量开辟空间,它和它引用的变量公用同一块内存空间。
类型&引用变量名 == (引用实体);
void TestRuf()
{
int a = 10;
int& ra = a;//--定义引用类型
printf("%p\n",&a);
printf("%p\n",&ra);
}
引用特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
//权限只能缩小不能放大
常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
//将d给r是隐式类型的转换,但是要给前面加上const
//rd就变成了中间变量的别名。
}
使用场景
1.做参数
void Swap(int& left,int& right)
{
int temp = left;
left = right;
right = temp;
}
2.做返回值
int& Test(int& a)
{
a+=10;
return a;
}
3.错误的返回值
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;//结果是7,而不是3
return 0;
}
注意
如果函数返回时,离开函数作用域后,其栈上的空间已经还给系统,因此不能用栈上的空间作为引用类型返回,如果以引用类型返回,返回值的生命周期必须不受函数的限制;
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
7.内联函数
概念
以inline修饰的函数叫做内联函数,编译时C++会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
特性
1.inline是一种以空间换时间的做法,省去调用函数额开销,所以代码很长或者有递归或循环的函数不适宜使用内联函数。
2.inline对于编译器来说只是一个建议,编译器会自动优化。
9. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
宏的优缺点
优点:
- 增强代码的复用性
- 提高性能。
- 没有压栈的开销
缺点:
- 不方便调试宏。
- 导致代码的可读性差,可维护性差,容易误用。
- 没有类型安全的检查。
例:
Add函数
#define Add(a,b) ((a)+(b))
Swap函数
#define Swap(a,b) (a = (a) + (b); b = (a) - (b); a = (a) - (b))
//或者
#define Swap(a,b)\
do{
int temp = a;\
a = b;\
b = temp;\
}while(false)
8.auto关键字(C++11)
概念
auto作为一个新的指示符来指示编译器,auto声明的变量编译器在编译时期推导而得。
1.用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
system("pause");
return 0;
}
注意
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类 型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为 变量实际的类型。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)//必须要用引用,要不然就不会修改原变量的值
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
auto不能推导的场景:
- auto不能作为函数的参数
- auto不能直接用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等 进行配合使用
- auto不能定义类的非静态成员变量
- 实例化模板时不能使用auto作为模板参数
9.基于范围得for循环(C++11)
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0])
; ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中 引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
范围for的使用条件
8. for循环迭代的范围必须是确定的 对于数组而言,就是数组中第一个元素和后一个元素的范围;对于类而言,应该提供begin和end的 方法,begin和end就是for循环迭代的范围。 注意:以下代码就有问题,因为for的范围不确定。
void Test(int array[])
{
for(auto& e : array)
cout<< e <<end1;
}
- 迭代的对象要实现++和==的操作。
10.指针空值nullptr(C++11)
在良好的C/C++编程习惯中,声明一个变量时好给该变量一个合适的初始值,否则可能会出现不可预料的 错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
system("pause");
return 0;
}
NULL在函数中被定义为0;
在C++中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议好使用nullptr 。
面试题:
1.下面两个函数能形成函数重载吗?有问题或者什么情况下会出现问题?
void TestFunc(int a = 10)
{
cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
cout<<"void TestFunc(int)"<<endl;
}
不是函数重载
2.c语言问什么不支持函数重载?
通过我们在写好一个.c/.cpp文件的时候,点击开始运行,那么它就能运行起来,运行一个程序就这一步就好了吗?其实当然不是,下面让我们来看看我们C/C++中我们的程序到底是如何运行起来的。大致可以分为4个阶段,如下:
- 预处理阶段(在预处理阶段,会进行宏替换,条件编译,头文件展开,去店掉注释)
- 编译阶段(在编译阶段,首先会进行语法语义检错,无误后要将我们写好的C文件编译成汇编文件)
- 汇编阶段(在汇编阶段就是要将我们的汇编文件转换成可执行的机器指令)
- 链接阶段(在链接阶段就是要把我们的所有的目标文件以及我们所依赖的库文件链接到一起生成可执行程序)
以上就是我们一个程序的执行过程,我们可以看到,在链接文件的时候,假如我们写了一个重载函数add,那么我们的重载函数在我们的C++底层中是如何处理的?很容易想到,在编译过程中,编译器会将我们的代码编译成汇编文件,这里其实就存在这一种重命名机制,我们把它就叫做名字修饰。
名字修饰内容
1、在C语言中,编译器在编译过程会将我们的函数重命名,具体的方法就是在我们的函数名前加上“_“修饰符,通过这种方式就可以在我们的符号表种查找到了,但是假如有两个相同的函数,编译之后进行相同的重命名,在符号表中生成的函数名一样,那么就无法区分到底是哪个函数了,所以这也就是我们的C语言为什么不支持函数重载的原因了。
2、在C++中,既然支持函数重载,那么它肯定对C语言在这方面进行了优化,具体的方法就是在我们的函数名后面加上参数然后生成我们符号表中的函数名称。那么这样一来,就很容易理解了,为什么C++可以支持函数重载了,就是因为函数重载底层的原理造成的,就是由于重载函数在符号表中生成的函数名称不一样,这样就能区分到底是哪个函数了。这样一来编译就能通过了。
3. C++中函数重载底层是怎么处理的?
底层的重命名机制将Add函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。
在C++调用约定(_cdecl 调用约定)中Add函数在底层被解析为:
“int __cdecl Add(int,int)” (?Add@@YAHHH@Z)
“double __cdecl Add(double,double)” (?Add@@YANNN@Z)
“long __cdecl Add(long,long)” (?Add@@YAJJJ@Z)
4.宏的优缺点?
优点:
1.增强代码的复用性。 2.提高性能。
缺点:
- 1.不方便调试宏。(因为预编译阶段进行了替换)
- 2.导致代码可读性差,可维护性差,容易误用。
- 3.没有类型安全的检查 。
5.C++有哪些技术替代宏?
- 常量定义 换用const
- 函数定义 换用内联函数