注意
:此篇博客只是C++概览,有很多知识点都待做补充
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为
C with classes
。
C++98
:C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
C++11
:增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等
C++14
:对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等
除此之外还有C++1.0,2.0,03,05,17,20等,但都不是那么重要
C++共有63个关键字相对于C的32个相当于翻倍(
兼容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 |
许多相同的变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题
下面是常规命名空间使用
namespace My_Space // My_Space为命名空间的名称 { // 命名空间中的内容,既可以定义变量,也可以定义函数 int a; int Add(int left, int right) { return left + right; } }
注意1
:命名空间可以嵌套namespace My_Space1 { int a; int b; int Add(int left, int right) { return left + right; } namespace My_Space2//嵌套 { int c; int d; int Sub(int left, int right) { return left - right; } } }
注意2
:同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中namespace My_Space // My_Space为命名空间的名称 { // 命名空间中的内容,既可以定义变量,也可以定义函数 int a; int Add(int left, int right) { return left + right; } } namespace My_Space // My_Space为命名空间的名称 { // 命名空间中的内容,既可以定义变量,也可以定义函数 int b; int Mul(int left, int right) { return left * right; } }
相当于
namespace My_Space // My_Space为命名空间的名称 { //上 int a; int Add(int left, int right) { return left + right; } //下 int b; int Mul(int left, int right) { return left * right; } }
1.加命名空间名称及作用域限定符’
::
'int main() { printf("%d\n", My_Space::a); return 0; }
2.使用
using
将命名空间中成员引入using My_Space::b; int main() { printf("%d\n", b); return 0; }
3.使用
using namespace 命名空间名称
引入using namespce My_Space; int main() { printf("%d\n", My_Space::a);//可以这样 printf("%d\n", b);//也可以直接使用 Add(10, 20); return 0; }
注意
:
使用cout标准输出(控制台)和cin标准输入(键盘)时必须包含< iostream >头文件以及std标准命名空间
关于C++中没有‘.h’的解释
:
早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中。后来将其实现在std命名空间下
,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h 旧编译器(vc 6.0)中还支持iostream.h格式,后续编译器已不支持,所以应使用的方式
+std
基本使用方法(粗略了解)
cout
相当于printf
cin
相当于scanf
endl
相当于换行符‘\n
’
cout可以自动识别变量类型(最多输出小数点后5
位)#include
using namespace std; int main() { int a; double b; char c; cin>>a; cin>>b>>c; cout<<a<<endl; cout<<b<<" "<<c<<endl; //不像printf,这里必须要用 输出流符号<< 来连接 return 0; }
定义
:调用函数时可传参可不传,不传时使用函数自己指定的实参,如例子所示:void TestFunc(int a = 0) { cout<<a<<endl; } int main() { TestFunc(); // 没有传参时,使用参数的默认值 TestFunc(10); // 传参时,使用指定的实参 }
void TestFunc(int a = 10, int b = 20, int c = 30);
void TestFunc(int a, int b = 10, int c = 20);
注意
:
半缺省参数必须从右往左依次来给出,不能间隔着给
缺省参数不能在函数声明和定义中同时出现
(如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用哪个缺省值
)缺省值必须是常量或者全局变量
C语言不支持(编译器不支持
定义
:函数重载是函数的一种特殊情况,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); return 0; }
以下不是函数重载
1.short Add(short left, short right) { return left+right; } int Add(short left, short right) { return left+right; }
2.
函数名修饰规则只是参数(个数 或 类型 或 顺序)不同和缺省参数没关系void TestFunc(int a = 10) { cout<<"void TestFunc(int)"<<endl; } void TestFunc(int a) { cout<<"void TestFunc(int)"<<endl; }
程序的运行,需要经历:
预处理、编译、汇编、链接
参考:C语言----程序编译(预处理)
在Linux下
:(假设a.cpp中调用了b.cpp中定义的func函数
)
链接阶段,链接器看到a.o
(二进制文件)调用func,但是没有func的地址,就会到b.o
(二进制文件)的符号表中找func的地址,然后链接到一起
在调用函数时一般会使用
汇编指令call
参见:函数栈帧的创建和销毁(详细)
形如
:80489bc: e8 73 ff ff ff call <?>
(问号也就是函数签名)
每个编译器都有自己的函数名修饰规则
gcc编译器
:
使用objdump工具,键入objdump -S 源文件
函数签名依然是func
这里我们已经可以得出结论
:
在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,构成函数签名
回到链接阶段
如果b.cpp中有两个func函数,由于在gcc编译器下会发现符号表中有两个重名的函数签名,所以会直接报错,因为不知道该拿出哪个地址。但g++就很明确了,两个相同函数名的函数签名不一样,所以可以轻松取出目标地址
这样一来,也可以说明上面不是函数重载的情况1(
与返回值无关
)
思考
:可以设计成返回值不同的重载吗?
答
:不能
- 如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能,在C++/C 程序中,我们可以忽略函数的返回值。在这种情况下,编译器和程序员都不知道哪个Function 函数被调用。
- 把返回值带进修饰规则,编译器层面是可以区分的,但是在语法调用层面,很难区分,具有严重歧义(因为你只是调用函数名和参数)
参考:
C++的函数重载
在Windows下
:参考:C/C++调用约定
extern "C" int Add(int left, int right);
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决
总而言之功能主要用在下面的情况:
- C++代码调用C语言代码
- 在C++的头文件中使用
- 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到
当C想用C++的接口(函数),C编译器直接用(
C++编译器用
exturn "C"
按照C规则编译出的
)二进制文件
《C++ primer》
定义
:引用是给已存在的变量取的一个别名,语法上程序不会引用变量开辟新内存,只是共用(一个变量可以拥有无数个别名)
void TestRef() { int a = 10; int& ra = a;//<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); }
特性
:
- 引用在定义时必须初始化,所以
不能为空引用
int a = 10;// int& ra; // 该条语句编译时会出错
- 一个变量可以有多个引用
如:int a=10;int& b=a; int&c =b; int& d=a
- 引用一旦引用一个实体,再不能引用其他实体
如:int a=10;int& b=a; int c=20; // b=c; //该条语句编译时会出错
例子
:int x=0,y=1; int*p1=&x; int*p2=&y; int*& p3=p1; *p3=10; p3=p2;
例子
:void TestConstRef() { const int a = 10; //int& ra = a; // 不可以 }
void TestConstRef() { int a = 10; const int& ra = a; // 可以 }
void TestConstRef() { double a = 14.56; int& ra = a; // 不可以,类型不同 }
注意
:
变成别名的条件:可以不变或缩小
原变量的读写权限,但不可以放大
读写权限
关联
:C语言----数据的存储(以前的博客写的不好,这里补充
)
C++语言编译系统提供的内部数据类型的隐式自动转换规则
如下:
- 执行算术运算时,低类型(短字节)可以转换为高类型(长字节);例如: int型转换成double型,char型转换成int型等等;
- 赋值表达式中,等号右边表达式的值的类型自动隐式地转换为左边变量的类型,并赋值给它;
- 函数调用时,将实参的值传递给形参,系统首先会自动隐式地把实参的值的类型转换为形参的类型,然后再赋值给形参;
- 函数有返回值时,系统首先会自动隐式地将返回表达式的值的类型转换为函数的返回类型,然后再赋值给调用函数返回。
在C/C++中
显式(强制类型)转换
和隐式类型转换
会产生临时变量
例
:void TestConstRef() { int i = 10; double d = i; const double& r = i; }
对于
double d = i;
:相当于i
->(临时变量(double)
)->d
对于const double& r = i;
:i
->(临时变量(double)
)->d
(double& r
只是临时变量的别名,而临时变量具有常性
(不是常量
),所以要加上const
)
C++ 临时变量的常量性 这篇博客说到:(临时变量并不是常量,只是编译器从语义层面限制了临时变量传递给非const引用,意在限制非常规用法的潜在错误)
以栈的接口StackInit和PrintStack为例:
StackInit(ST& s); PrintStack(const ST& s);
传引用
:
- 输出型参数 为了形参改变实参(代替C的指针)
- 传引用减少拷贝(和传指针一样,传值会开辟同样大的一块空间)
- PrintStack这种只读函数中建议传const引用
1)可以防止函数逻辑中出现"==“写为”="的情况,保护形参不被修改,
2)同时既可以传普通对象,也可以传const对象
关联传址和传值
)以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低
一般情况
:是传值返回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; return 0; }
所谓传值返回
:(函数销毁了函数里的c也被销毁了,只有通过临时变量)
- 当return的值占空间较大的时候,会在main函数中开辟一块临时空间
- 当return的值占空间较小时,通常是用eax寄存器传递
引用返回
:引用作为返回值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; return 0; }
当前代码的结果是不确定的,取决于平台销毁栈帧时会不会清理栈帧空间,在平台(VS)不清理栈帧空间的情况下,(此时ret =Add(1,2)就越界访问,但编译器对越界的行为只是抽查 详见:C语言----操作和注意点(待补充))
当我们知道在VS平台不清理栈帧空间后
进一步改变代码
:int Add(int a, int b) { int c=a+b; return c; } int main() { int& ret=Add(1, 2);//这里ret改为引用 Add(3,4); cout << "Add(1, 2) is :" << ret << endl; return 0; }
- ret改为引用,ret就是c的别名,打印ret就是打印c空间的值,结果是7
- 这是因为调用了一次Add(3, 4),此时c的空间变成了7,在函数栈帧销毁时,VS平台仍未清理栈帧空间,所以打印ret为7,进一步提现了非法访问的危害
- 如果把Add(3, 4)改为任意其他的一个函数,结果就会变成了一个随机值
结论
:
- 出了func函数作用域,
ret被销毁,就不能使用引用返回
- 出了func函数作用域,
ret不被销毁,就可以使用引用返回
1. 当ret是static变量
例如
:int& Count() { static int n = 0; n++; // ... return n; }
出了作用域static变量n还在,可以引用返回
2. 待补充…
3. 待补充…
引用作返回值的意义
:
- 价值,减少拷贝
- 方便实现operator[] (
以后补充
)
在
语法概念
上引用不同于指针,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
在
底层实现
上实际是有空间的,因为引用是按照指针方式来实现的
这里有一段代码:int main() { int a = 5; int& ra = a; ra = 20; int* pa = &a; *pa = 30; return 0; }
引用和指针的几点区别
:
- 引用在定义时
必须初始化
,指针没有要求- 引用在初始化时引用一个实体后,就
不能再引用其他实体
,而指针可以在任何时候指向任何一个同类型实体没有NULL引用
,但有NULL指针- 在sizeof中含义不同:引用结果为引用类型的大小(
与类型有关
),但指针始终是地址空间所占字节个数(32位平台下占4个字节)(与类型无关
)- 引用自加即引用的实体增加1(
与类型无关
),指针自加即指针向后偏移一个类型的大小(与类型有关
)- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,
引用编译器自己处理
(即上面提到的汇编层)- 引用比指针使用起来
相对
更安全
在C中是利用宏来减少函数栈帧空间的开销的
由于宏
(参考:C语言----程序编译(预处理)):
- 语法复杂,细节多,易出错
- 没有类型安全检查
- 不能调试
inline定义
: 为了弥补宏的缺陷,C++以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数栈帧空间的开销,内联函数提升程序运行的效率
C++建议用const/enum代替宏常量,inline代替宏函数
用法
:inline int Add(int x, int y) { return x+y; }
注意
:
内联函数的特性
:
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长(
一般大于10行
)或者有循环/递归的函数不适宜使用作为内联函数- inline对于编译器而言
只是一个建议
,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略内联
- inline不建议声明和定义分离(
声明和定义应放在一个源文件里
),分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到(编译器不会为内联函数生成地址,因为是直接展开
)
C++11中,auto不再是C++98中的一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
注意
:
使用auto定义变量时必须对其进行初始化
,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型,
缺点
:一定程度牺牲了代码的可读性
了解:
typeid(a).name
可以查看a的变量类型
auto的使用:
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 } //此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) { //... } void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6};//编译失败,auto不能直接用来声明数组 }
注意
:
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
- auto不能作为函数的参数
- auto不能直接用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
用法
:如下
e为数组里每个值的别名
(依次取数组中的值赋给e,自动结束,第二次for循环打印的值全×2)
没有加&
第二次for循环打印就不会改变void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array) // 加了&引用 e *= 2; for(auto e : array) cout << e << " "; return 0; }
在传统C中 ‘
NULL
’ 是 头文件 stddef.h 中的一个宏,可能被定义为字面常量0
,或者被定义为无类型指针(void*)
的常量,C++98也用的NULL#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
所以我们在遇到这样的代码:
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(NULL); f((int*)NULL); return 0; }
第一次打印的是f(int)与我们想打印
空指针
相悖,只有第二次强制类型转换才可以达到预期效果
所以在C++中引入了专门用来表示空指针的nullptr
注意
:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr