在这一章中,我们来认真学习一门全新的语言,C++,尽管可能会有些琐碎复杂,但只要我们认真细心,记好每一个细碎的知识点,将知识汇总成网状结构,慢慢来,就可以学好,不过,也正如那句话而言,C++不仅仅是一门语言,更是一种生活哲学,要想更加透彻的理解C++,还需循序渐进,慢慢体会
C++一共有63个关键字,C语言则有32个,在这里我们仅对关键字进行展示,对于关键字的详细展开,我们在后面再进行分析
一共这么63个关键字
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 ,以 避免命名冲突或名字 污染 , namespace 关键字的出现就是针对这种问题的。
其实意思就是在C++中,不同的内存可以分别定义不同的命名空间,而在这些不同的命名空间中可以定义同名的变量,函数等,此时这些变量函数等就不会冲突
定义命名空间,需要使用到 namespace 关键字 ,后面跟 命名空间的名字 ,然 后接一对 {} 即可, {} 中即为命名空间的成员。
//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
// 命名空间中的内容,既可以定义变量,也可以定义函数
int a;
int Add(int left, int right)
{
return left + right;
}
}
//2. 命名空间可以嵌套
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意: 一个命名空间就定义了一个新的作用域 ,命名空间中的所有内容都局限于该命名空间中
namespace N {
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
int Sub(int left, int right)
{
return left - right;
}
}
int main()
{
printf("%d\n", a); // 该语句编译出错,无法识别a
return 0; }
注意:上面代码中的a是定义在N这个命名空间中的,没有在main函数中,所以在我们的main在直接使用a时无法找到a
那么如何才能对a进行使用呢?
我们使用命名空间有三种方式
int main()
{
printf("%d\n", N::a);
return 0;
}
我们只需要加上 N::a这个语句,就相当于对其进行了说明,说明a是属于N的,之后便可以使用了
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
我们可以在main函数外,利用using关键字对N::b进行声明,相当于using在全局将b进行了展开,之后便可以当成常规变量使用了
using namespce N;
int main()
{
printf("%d\n", N::a);//展开之后也可以进行声明
printf("%d\n", b);
Add(10, 20);
return 0;
}
这种方式是直接用using namespce N将N全部展开,所以在N中的成员就不需要再进行声明,可以直接使用
使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。
注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带.h ;旧编译器 (vc 6.0) 中还支持格式,后续编译器已不支持,因此 推荐 使用 +std 的方式。
我们在C语言中,每次书写代码时都会在开头包上一个#include
#include
//using namespace std;//C++库中的所有东西都是放到std命名空间中的
int main()
{
std::cout << "hello word\n";
std::cout << "hello word" << std::endl;
return 0;
}
在上述代码中,我们未引入using namespace std,所以我们在使用输入输出流时需要在前面加上std::这样的语句说明cout是std中的函数,而上面的两个语句达到的都是一样的效果,换行
对于上述代码,我们可以看到,cout函数对于数据有类型自动识别功能,就不需要我们手动的去像C语言一样操作%d,%s,比较方便
使用 C++ 输入输出更方便,不需增加数据格式控制,比如:整形 --%d ,字符 --%c
而在上面情况下,我们每次在调用cout函数还需要在前面带上一个std::,太过于麻烦,那么此时我们就可以在开头引入std
但是需要注意的是,当我们引入了std,那么我们就要避免去定义与std中具有相同名的函数或者变量
当我们定义一个与std中cout函数相同名字的变量时,我们std中的cout函数便无法使用,这里便会有报错,正确的做法是避免去定义名称与std中内容冲突的东西
还有一种定义方式是仅对我们所需要的函数进行说明
在上述代码中我们仅对cout与endl函数进行了声明,所以就可以使用,其他函数还是无法使用,但是需要注意的是,在声明过cout与endl函数之后,仍然不能取cout与endl的同名变量
当我们介绍完cout这个函数,这是我们的输出函数,同C语言的printf一样,我们还会有输出函数,cin,效果与scanf一样
这段代码的含义就是引入一个输入流到i,到d,那么这时i与d的值就被更换为12 与123了,同我们的scanf一样的效果,我们日后在工程中对于std库采用的策略是:展开库中的常用的一些对象或者类型,并不完全展开
缺省参数是 声明或定义函数时 为函数的 参数指定一个默认值 。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
我们可以看到,缺省参数实质上就是在形参的位置提前写上一个默认值,这里的默认值为0,那么当我们调用这个函数时,若不传参,则使用的就是默认值,传参才使用指定的参数
当我们给一个函数全部设置缺省参数时,此时传一个参数,传两个参数,传三个参数,会有不同的效果,总的来说就是那个形参没有传参,就会用缺省参数来替代
还有一种是半缺省,就是仅对部分形参传入缺省参数,那么此时a,b就必须需要传入参数,否则会报错,而c则可以不传,不传时用缺省参数,传时就用传的参
还需要注意的是在半缺省时必须从右往左连续使用缺省函数才可以,不可以间隔缺省
1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给2. 缺省参数不能在函数声明和定义中同时出现
函数重载 : 是函数的一种特殊情况, 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;
}
上面Add函数的三种调用方式构成了函数重载
int Add(int left, int right)
double Add(double left, double right)
long Add(long left, long right)
而在我们主函数中会根据你输入的参数不同来调用不同的重载函数,会自行识别
什么是函数重载:在C++中,函数名相同,参数不同,参数可能顺序,类型或个数都有可能不同,只要满足其中一个条件就可以构成函数重载,而对返回值没有要求
当我们了解了C++的重载了之后,我们来思考一个问题,为什么C语言语法不允许重载,而C++却可以呢?
那么C++是如何支持函数重载的呢?
让我们进入操作系统,分别对C与C++进行编译,在编译链接中会进行以下几个过程
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
list.h list.c test.c
1.预处理->头文件展开/宏替换/条件编译/去掉注释
list.i test.i
2.编译->检查语法,生成汇编代码
list.s test.s
3.汇编->汇编代码转成二进制机器码
list.o test.o
4.链接->将两个目标文件链接到一起->生成可执行程序
那么我们就从一个程序进行编译时来了解
首先在我们程序中通常都会有list.c与test.c文件,他们都包含了list.h文件,其中,lest.c中负责了函数的定义,list.h负责了函数的声明,而tist.c中调用主函数,仅仅包含list.h也只有声明,在我们预处理完毕将test.c与list.c文件转为test.o与list.o文件时,会对函数有call说明,但是在test.o中仅包含了函数的声明,所以我们call函数时仅有函数名,而后面应该填地址的地方因为找不到定义,所以填的是问号?,而在list.o中则因为存在函数的定义,所以call时就找的到函数真正定义的地方,所以会有地址的显示,可以在汇编中直接找到函数定义的地方。
下一步链接,链接时就需要将两个函数转化为可执行文件,那么具体如何实现的呢?事实上,链接的过程,就是进入到test.o文件中,在符号表中记录各个函数的调用地址,而因为编译中没有某些函数的定义,仅有声明,这些函数在call时填入的是?,符号表中无法查询到,所以在链接时就需要带着函数名去别的文件的符号表中查询,去list.o文件中查询,顺利查到后将函数地址写入test.o的call中,此时test.o才是一个可执行文件
而我们这个过程又与重载又有什么关系呢?
事实上,我们需要回到test.o去list.o中带着函数名去查找地址这一步,在C语言中,函数在编译之后名称不会发生改变,所对应的地址也仅有一个
所以当我们的test.o去lest.o中查找时仅查得到这一个地址
那么我们再来看一看C++编译之后
通过下面我们可以看出 gcc 的函数修饰后名字不变。而 g++ 的函数修饰后变成【 _Z+ 函数长度 + 函数名 + 类 型首字母】
在C++编译后,我们可以发现,函数名称发生了改变,后面加了形参的缩写,因为这个改变,使得我们的重载得以进行,因为名称发生了改变,所以当我们在执行相同函数名,不同形参时,g++编译完成后因为名称改变的缘故,所以我们同一个函数会有多个不同的名称和地址,而C语言中不改变名称则仅有一个地址,那么此时test.o去list.o中查询时,在C++中就可以找到同一个函数的多个不同名地址,而在C语言中只能找到一个,这也就是为什么C语言不支持重载,最关键的原因就是编译后不改变函数名仅有一个地址,合并时找不到重载函数,仅能找到第一个定义的函数,而C++中因为有函数名的改变,同函数会有多个地址,合并时可以找到多个重载函数,完成合并,进而生成可执行文件。
而在windows系统中C与C++的命名规则比较诡异,这里我们就仅展示Linux的,但是道理都是一样的
通过这里就理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区 分,只要参数不同,修饰出来的名字就不一样,就支持了重载 。另外我们也理解了,为什么函数重载要求参数不同!而跟返回值没关系。
有时候在 C++ 工程中可能需要 将某些函数按照 C 的风格来编译 , 在函数前加 extern "C" ,意思是告诉编译器, 将该函数按照 C 语言规则来编译 。比如: tcmalloc 是 google 用 C++ 实现的一个项目,他提供 tcmallc() 和 tcfree 两个接口来使用,但如果是C 项目就没办法使用,那么他就使用 extern “C” 来解决。
我们假设一种情况:我们用C++实现一个东西,编译成动态库,那么我们使用C++来调这个库,是没问题的,但如果我们想让C程序调用这个动态库呢?答案是不能直接调用,链接时会出现问题,因为我们C语言中是使用tcmlloc去寻找函数的,可以找到C语言修饰的函数名,而C++修饰的函数名与C语言不一样,无法找到
那么我们如果想让C语言程序去调用C++中的库呢?想让这个库既可以被C语言链接,也能被C++链接,此时便需要我们的extern "C",我们用下面这个例子来说明,原来的C++程序调用C++的库只需要引入tcmalloc.h就可以,而我们想让C程序调用这个就需要引入extern "C"void tcmalloc(size_t n)就可以找到了
因为有extern "C"我们的符号表就被改成了C语言的命名方式,C语言就可以直接对其进行访问了,而我们的C++程序在扫描这个符号的时候,发现前面有extern "C",便会切换到C语言的名称查找方式在链接时查找
引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
事实上,引用,通俗的来讲就是给一个对象起了另一个名字,就像小名一样,小名与大名都是你,而且小名(引用)可以有多个
如上面这段代码,我们给a取了ra,b,c三个名字,都代表的是a这个变量,在物理层面中指的是一块地址空间,而我们在后面对c进行了修改,实质上也就是对这一块内存修改了,ra,b,a也就都修改了
1. 引用在 定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体
值得注意的是,引用必须要在定义时初始化
我们上面这段代码,c=d这个语句其实就是将2赋值给了a,引用只要确定了一个引用对象就无法改变
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; //加const后与隐式转化中开辟的常性临时变量类型一致,引用的对象是临时变量,而非d
}
注意这段代码,当我们定义了const c之后,就不能使用 int&d等对其引用,因为类型不一样,要想对c取别名,只能给d也加上const,使他们类型相同,才可以
上面这一种就可以,因为权限缩小了,c是可读可写,引用e只可读,可以进行引用
我们再来看看这种情况,当我们想用double类型的变量对i进行引用会发生报错,但是加上const就可以,原因是当int型的i赋给double时会发生隐式类型的转换,当int型i转化为double型 db时,会创建一个临时的double变量,而临时变量具有常量性,所以加上const是与临时变量的类型统一了,所以加上const之后便可以进行引用了
权限的放大缩小规则:仅适用于引用和指针,(同一对象的操作),并不适用于变量间的赋值等操作(多个对象间的)
1.做参数
void Swap1(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void Swap2(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
这是我们所熟知的交换函数,第一种,通过交换指针,从而直接影响外面的实参,完成交换,而第二种,就是我们的引用,直接将外面实参的别名做参数,形参实例化时不会发生拷贝,直接就是外面的实参,也能完成交换
2.做返回值
int Count()//传值返回(拷贝),传回了一个临时变量tmp,多产生了一个空间
{
static int n = 0;
n++;
return n;
}
int& Count()//传引用返回,直接返回的n的引用,tmp,没有额外的空间开辟
{
static int n = 0;
n++;
return n;
}
事实上,因为第一个传值返回返回的是一个临时变量,所以我们对他们的返回值进行在引用时无法引用,因为临时变量具有常性,需要加上const缩小权限才可以,而第二种传引用返回值则不会有这样的问题,因为没有临时变量的开辟,没有常性,直接就是int
注意:传值传参与传返回值都会产生一个临时变量,而传引用则不会,而临时变量都具有常性,所以再引用他们时就需要加上const来保持权限规则
我们再来看一个例子
咦?为什么我们的结果会变成7呢?不是预想的3吗?
其实,这就是返回值传引用的一个弊病,当我们走完Add2(1,2)时,返回的ret是3没错,但是我们返回的是3本身的那块内存,而非他的拷贝,这就导致了我们再次调用Add(3,4)时,有了新的返回值7,就将原来上一步的ret结果重新用7给附上了,同一块空间又在第二次调用函数时被改变了,所以我们的传引用返回在返回值变量是个局部变量时不安全是不安全的,因为后续相同函数的调用会影响先前的返回值
那么我们应该如何来解决这个问题呢
我们只需要在函数中加上static就可以,改变它的生命周期,让其不要存放在函数栈中,使这一句语句只会在定义处被执行一次,以免被后面的函数再次调用
总结:一个函数要使用引用返回,返回变量出了这个函数的作用域还存在,就可以使用引用返回,否则不安全
在上述例子中就是:未加static之前:函数调用完毕,栈被销毁,c随之销毁,再次调函数开启栈帧再次找到c的内存复用,会改变这个c的值,加上static之后:仅被执行一次,c被存在静态区中,再次调函数开启栈帧,找不到之前调用的c的内存,不改变这个c
函数使用引用返回的好处?->少创建一个临时对象,提高效率
值和引用作为返回值类型的性能比较
我们可以看到,不管是传引用做形参,还是传引用做返回值,在函数调用十万次时差距还是很大的
此时我们可以看到,传引用对我们而言,1.做输出型参数,2.做返回值,3.提高效率....
在 语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在 底层实现上 实际是有空间的,因为 引用是按照指针方式来实现 的。
底层汇编实现引用与指针:
我们可以看到底层其实引用与指针几乎相同,都是将地址赋给一个变量
引用和指针的不同点 :1. 引用 在定义时 必须初始化 ,指针没有要求,因为引用必须要有对象才能引用,指针可以赋随机值2. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体3. 没有 NULL 引用 ,但有 NULL 指针4. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4个字节 )5. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小6. 有多级指针,但是没有多级引用7. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理8. 引用比指针使用起来相对更安全
我们在写程序时总会遇到一些我们特别常用的函数,就像我们的加法函数Add,交换函数Swap等那么重新去调用,那么在我们程序运行时再去展开这些函数,开辟栈帧,用完之后销毁栈帧,消耗非常大,那么有没有一种方法可以直接去用这些函数,而不再在运行时调用呢?是有的
在c语言中为了解决这个问题采用的是宏函数,而在我们的c++中,就是我们的内联函数
以 inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数压栈的开销,内联函数提升程序运行的效率。
在我们直接调用这个函数时,还有call语句,意味着还没有进行展开,仍然需要消耗才能使用这个函数
此时将函数改为内联函数之后,我们可以看到函数在运行之前,编译期间就被展开了,我们在程序中使用这个函数就不会有消耗,提高了效率
1. inline 是一种 以空间换时间 的做法,省去调用函数额开销。所以 代码很长 或者有 循环 / 递归 的函数不适宜使用作为内联函数。2. inline 对于编译器而言只是一个建议 ,编译器会自动优化,如果定义为 inline 的函数体行数比较多 / 递归等等,编译器优化时会忽略掉内联。3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
总的来说就是内联函数提前将函数展开了,扩大了代码的行数,但是省去了开辟栈帧这种有消耗的操作
但是一般内联适合于小函数,在碰到递归,或者比较长的函数,就不适合,因为这些冗余的函数展开之后空间消耗过大,编译器就会自行判断,省去内联
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 ,但遗憾的是一直没有人去使用它C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得 。
int a = 10;
auto b = a;//b的类型根据a去进行推导,此时推导出来的就是int
auto实际的用法就在这里了,并不复杂,就是随着赋值变量去推导类型而用的,当a的类型改变时,b也就会随着而改变
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类 型 。因此 auto 并非是一种 “ 类型 ” 的声明,而是一个类型声明时的 “ 占位符 ” ,编译器在编译期会将 auto 替换为 变量实际的类型 。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量 。
auto a = 1, b = 2;
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};
}
我们若想对一个数组进行各个元素*2并且打印,那么我们之前的写法为
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是如何操作的
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0; }
我们的新代码简洁就了许多
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。注意:以下代码就有问题,因为 for 的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <
在我们C语言中,我们总是使用NULL来代表空指针,但实际上NULL是一个宏,代表的是int型0元素
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在我们头文件中就可以看到
可以看到, NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦
比如当我们书写如下代码时
我们原先预想的NULL是会匹配到指针类型去,但实际上匹配到了int型,这一问题的存在容易使我们在别的地方遇到一些不可预料的错误,所以C++11引入了nullptr
实际上我们的nullptr也还是0,只不过进行了一些处理我们将其变为(void*)0转成了void*类型
字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0 。
注意:
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的 。2. 在 C++11 中, sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr 。