到这里,本喵的C语言学习暂时就告一段落了,开始C++的学习了,同样的,本喵会在《C++学习》专栏中记录本喵的学习过程,分享自己的所学所得,下面开始C++的介绍。
- C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言
应运而生。- 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本。
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即:计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等 |
C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
C++23 | 制定ing |
- C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所以本喵这里介绍的主要也是C++98和C++11
总得来说,C++就是在弥补C语言的一些不足之处,所以它是完全兼容C语言的,而且在C语言的语法基础上产生了一些新的语法和新的用法。
下面我们来看看C++中的新语法。
C++总计63个关键字,C语言32个关键字
这里只是看一下C++有多少关键字,不对关键字进行具体的讲解,这些关键字的介绍以及讲解会贯穿在后面的学习中。
- 在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
- 使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
正常的命名空间定义:
来看一段程序:
- 在C语言的学习中我们知道,当局部变量和全局变量重名的时候,优先使用局部变量
- 所以这里的打印的结果是局部变量a的值,而不是全局变量a的值
这里就涉及到一个程序在编译时符号名的查找顺序,根据上面程序,我们知道,查找的顺序是先查找局部的,当局部中没有时再查找全局的。
- 这里我们包含了一个头文件stdlib.h,这个头文件中有一个函数就是rand(),就像编译器警告中所描述的那样
- 还创建了一个全局变量rand,该全局变量的名字和函数rand的名字是相同的
- 我们知道,在预处理阶段,stdlib.h中的内容会全部复制到main所在的源文件中,此时在该源文件中就出现了俩个rand符号,而且表示函数的rand出现在全局变量rand之前,所以在制作符号表的时候就无法正常进行,编译器就会报错。
- 按照我们C语言中所学习的,自己定义的符号名是不能重名的,而且也不能和关键字相同,遇到这种情况我们只能给全局变量换一个名字
但是在C++中,像上面这种符号名重复的情况是可以的,这个时候就需要使用到C++中的域,这里是指命名空间。
- 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
上面重名的这种情况就可以将全局变量rand放在命名空间中,代码如下:
namespace wxf
{
int rand = 10;
}
int main()
{
printf("%d\n", wxf::rand);
return 0;
}
namespace关键字:
- namespace 后面跟的是命名空间的名字,中间必须用空格隔开
- 在使用命名空间中的变量名时,需要用到俩个冒号(:: ),叫做域作用限制符,要使用到命名空间中的哪个变量,域作用限制符::后面就跟哪个变量的名字
通俗一点的理解就是,命名空间相当于给我们创建的变量建了一个围墙,别人是看不到围墙里的东西的,只有通过域作用限制符(::)将围墙里的东西告诉外面的人,别人才能知道里面有这么一个东西。
- 命名空间的作用就是改变编译器的查找符号名的顺序
- 没有命名空间时,编译器查找符号名是先查找局部,局部没有再查找全局
- 使用了命名空间后,如果不使用域作用限制符,这个命名空间还是相当于没有,编译器查找符号名的顺序和以前一样
- 当使用域作用限制符时,编译器就不采用之前的查找方式,而是直接中命名空间中查找::后面的符号名
我们在C语言中是尽量避免使用全局变量的,因为全局变量很有可能在某个函数中使用到,从而造成全局变量的修改,而C++中使用了命名空间就很好了避免了这个问题。
比如要统计学校中大一大二和大三各个年级的人数:
- 这里大一大二大三的人数都用sum来表示,都是全局变量,而且变量名是相同的,但是它们在不同的命名空间中,所以在使用的时候并不会造成混乱。
有了命名空间,我们在变量命名的时候就不用再顾及那么多了。
命名空间可以嵌套:
当然,命名空间也是支持嵌套的,比如表示机械学院中大一大二大三中各个年级各个班中的人数。
namespace mechanic_college
{
int sum = 37000;
namespace freshman
{
int sum = 10000;
namespace class1
{
int sum = 6000;
}
namespace class2
{
int sum = 4000;
}
}
namespace sophomore
{
int sum = 12000;
namespace class1
{
int sum = 5000;
}
namespace class2
{
int sum = 7000;
}
}
namespace junior
{
int sum = 15000;
namespace class1
{
int sum = 8000;
}
namespace class2
{
int sum = 7000;
}
}
}
int main()
{
printf("大一共有%d人,一班有%d人,二班有%d人\n", mechanic_college::freshman::sum,
mechanic_college::freshman::class1::sum,
mechanic_college::freshman::class1::sum);
printf("大二共有%d人,一班有%d人,二班有%d人\n", mechanic_college::sophomore::sum,
mechanic_college::sophomore::class1::sum,
mechanic_college::sophomore::class1::sum);
printf("大二共有%d人,一班有%d人,二班有%d人\n", mechanic_college::junior::sum,
mechanic_college::junior::class1::sum,
mechanic_college::junior::class1::sum);
return 0;
}
- 在命名空间中,创建了很多的全局变量,它们的名字都是sum,但是在使用的时候并不存在重复使用的情况,就是因为每个sum所属的命名空间不同。
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中:
在命名空间中,不仅能够定义变量,而且可以定义函数:
知道这里为什么报错吗?
因为将函数Add定义到了命名空间中,没有使用域作用限制符告诉编译器这里有Add函数,所以编译器找不到我们定义的这个函数。
这样就能正常打印出我们要的结果30了。
在C语言中写函数的时候,定义和声明是分开的,那么对于这种情况,命名空间是怎么使用的呢?
还是和C语言中一样的使用方法,只是将我们所写函数的声明和定义放在了命名空间中。
- .h文件和.cpp文件中的命名空间必须一样
- 在编译阶段,编译器会将不同源文件,头文件中的同名字命名空间进行合并
加命名空间名称及作用域限定符:
这种方法在上面介绍命名空间的时候已经详细介绍过了。
使用using将命名空间中某个成员引入:
可以看到,我们使用命名空间中的变量的时候,命名空间的名字需要在每个变量名前面都写一次,这样做感觉的有的繁琐,如果使用的变量有100个呢?此时就需要写100次命名空间的名字。
有没有一种办法,像C语言一样,直接使用变量名就行呢?C++中提供了一种方法。
- 在要使用命名空间变量的程序之前写一个语句
- using 命名空间的名字::变量名
只有加上这句话,在后面的程序中就可以直接使用命名空间中的该变量,相当于将该变量放在了围墙外面。
还是上面的代码,可以看到,此时就不需要再写变量名所在的命名空间了,直接像C语言中一样使用变量就可以。
使用using namespace 命名空间名称引入
是否又发现了一个问题,现在是可以将命名空间中的某个变量在程序中直接使用了,不用再加命名空间,那么,如果命名空间中有1000个变量,并且都要使用呢?难道要写1000次上面那个语句吗?
这里C++又提供了一种方法
- 在程序的前面写一个语句
- using namespace 命名空间的名字
此时,命名空间中的所有变量都可以在下面的程序中直接使用了,都不用再写命名空间的名字了,也不用挨个将命名空间中的变量释放出来。
仍然是上面的程序,使用该方法后,命名空间中的所有变量在使用的时候都不需要使用域作用限制符了,也就不用写多次命名空间的名字了,否则光是变量名就非常的复杂。
注意:
在C++程序中,我们通常在程序的最前面会看到 using namespace std 这样一个语句,这个语句的意思是什么呢?很多小伙伴都认为它是C++程序中必须有的,记住就行了,只要写C++程序就需要写这么一句话,这是错误的。
在上面我们也写了函数,而且是放在命名空间中的,C++中也有很多的库函数,像cout,cin等等,就类似于C语言中的print和scanf这样的库函数,官方提供的库函数都写在了一个名字为std的命名空间中。
- using namespace std的意思就是将官方库函数所在命名空间的墙拆掉,在程序中直接像C语言一样使用库函数就行,不用再通过域作用限制符来调用。
下面看一个例子,回想一下,在学任何编程语言时,最开始都是在屏幕上打印hello world,我们就用这个来举列:
#include
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
它们的结果都是一样,都是打印出了hello wrold,区别就在于:
在上面我们用C++的语法打印出了hello world这个问候语,下面再演示输出一些别的内容。
输出数据
#include
using namespace std;
int main()
{
int a = 10;
int b = 20;
cout << a + b << endl;
return 0;
}
对比C语言中的printf你发现有什么不同了吗?
- cout输出中,它能自动识别变量的类型,如果使用C语言中的printf的话,就需要以%d的形式输出,而cout只需要直接写变量名就行。
- 但是当输出的数是格式化的数据时,cout来控制格式就不方便了,需要使用C中的printf来控制输出的格式
int main()
{
double pi = 3.1415926f;
cout << pi << endl;
printf("%.2lf\n", pi);
return 0;
}
使用C中的printf很容易的就将输出控制在了小数点后俩位,而cout则无法控制。
所以说,到底使用cout还是printf需要我们自己来决定,哪个方便用哪个,因为C++是完全兼容C语言的。
再来看一个输入的例子:
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
int c = a + b;
cout << c << endl;
return 0;
}
cin和C语言中的scanf是一样的,只它也不能进格式化控制。
这里有几点说明:
使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
<<是流插入运算符,>>是流提取运算符。这里可以形象的理解,cout<>a,就是从cin控制台流到了a中。
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识在后序本喵会和大家分享,所以我们这里只是简单学习他们的使用。
来看一段代码:
void func(int a)
{
cout << a << endl;
}
int main()
{
int a = 10;
func(a);
return 0;
}
这里将打印a的值封装在一个函数func中,在main函数中将实参a的值传给了形参a,从而打印出a的值。
如果这里不给形参会发生什么?
我们知道,在C语言中,必须有实参传过去才行,否则就会报错,但是在C++中就可以不传实参。
void func(int a = 10)
{
cout << a << endl;
}
int main()
{
func();
return 0;
}
我们再来看缺省参数的概念:
- 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
再来看一段代码:
void func(int a = 10)
{
cout << a << endl;
}
int main()
{
int a = 20;
func(a);
return 0;
}
这段代码的结果是什么?是10还是20,其中10函数func的缺省参数,20是调用func传递的实参。
根据定义就可以得出,有缺省参数的函数,在调用的时候,如果没有实参传入,那么在函数中就使用缺省参数,如果有实参传入就使用传入的实参。
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
这种情况下,函数的形参全部都是缺省参数,有无实参传入都可以。
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注意:
在使用缺省参数时,还有几个需要注意的事项,这是针对所有含有缺省参数的函数的。
正确的样子如上图中所示,在头文件中写函数的声明,此时有缺省参数,在Add.cpp中写函数的定义,此时没有缺省参数,在test.c中调用该函数,并且没有传入实参。
为什么不能在函数的声明和定义中同时写缺省参数呢?
来看下面的这张图:
此时,函数声明中的缺省参数和函数定义中的缺省参数是不同的,当调用函数时没有传入实参,那么此时编译器该听谁的?听函数声明的还是听函数定义的?答案是听函数声明的。
可以看到,结果还是30,说明编译器是使用的是函数声明中的缺省参数。
所以在函数声明和函数定义中都有缺省参数的情况时,仅在函数声明中写就可以了。
既有函数声明又有函数定义,但是将函数的缺省参数只写在了函数定义中,函数的声明中没有,所以就报错了。
编译器此时是听函数声明的,它认为这个函数没有缺省参数,所以必须有实参传入。
常量我们在上面已经看到了,下面我们来看变量的情况:
此时缺省参数的值采用的是全局变量a和b的值,结果和我们预想的一样是30。
再看局部变量的情况:
当缺省参数是局部变量的时候,编译器直接报错,缺省值是未定义的。
说了那么多,缺省参数有什么作用呢?你可能觉得我以前C语言中的实参和形参对应就挺好啊,这个缺省参数有什么用呢?
那栈来举列,来看栈初始化的代码:
我们在对栈初始化的时候,需要开辟动态空间,这个空间该开辟多少个呢?
在【数据结构】栈和队列中我们采用的是realloc实现的,每次插入数据时都需要判断一下空间是否够用,不够用就扩容。
但是扩容是有代价的,原地扩容还好,只是在原来的空间的基础上再增加一些空间,但是异地扩容就需要将原本的数据在复制到一个新的位置。
我们这里采用在初始化的时候就将栈的空间开辟好,这样就避免了扩容,从而避免了扩容产生的不好后果。如上图中的代码,我们这里开辟了100个int型的空间。
可以看到,我们必须提前知道需要多少空间才能进行传参,进而开辟动态空间,但是如果我们不知道需要多少空间呢?
这个时候就用到缺省参数了:
这里我们在调用初始化函数的时候是不知道需要开辟多少空间的,所以就没有传个数的参数,但是在函数的定义中使用了缺省参数,这个缺省参数也就是开辟空间的默认值,默认是10个,此时就不用纠结到底开辟多少个了。
当我们清楚的知道需要开辟多少个空间的时候也可以直接将实参传过去。
- 自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
- 比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载也是这样,即一个函数名对应着多个函数,如:
#include
void Print(int a)
{
cout <<"整型数据:"<< a << endl;
}
void Print(float b)
{
cout << "浮点型数据:"<< b << endl;
}
int main()
{
int a = 10;
float b = 3.14f;
Print(a);
Print(b);
return 0;
}
上面代码中,定义了两个函数,一个是打印整型数据的,一个是打印浮点型数据的,但是它们的函数名是相同的,在main函数中,只要在调用时将相应的实参传入就会调用相应的函数。
这种情况在C语言中是绝对不被允许的,函数名是不可以重名的,只能打印整数的函数用一个函数名,打印浮点数的函数用另一个函数名。
这就是C++中的函数重载。
在调用该函数的时候,实参是int类型和int类型的时候,调用的就是形参是int和int类型的Add函数。
实参是double类型和double类型的时候,调用的就是形参是double和double类型的Add函数。
这对重载函数的参数类型不同。
上图中的代码,两个Add函数的参数个数是不同的,上面的参数个数是0个,下面的参数个是1个。
在调用该函数的时候,实参是没有参数的时候,调用的就是形参个数是0的类型的Add函数。
实参个是1个的时候,调用的就是形参个是1个的类型的Add函数。
这对重载函数的形参个数是不同的。
上图中的代码,两个Add函数的参数的类型顺序是不同的,上面的参数类型顺序是int,char,下面的参数类型顺序是char,int。
在调用该函数的时候,实参的类型顺序是int,char的时候,调用的是形参类型顺序是int,char的func函数。
实参的类型顺序是char,int的时候,调用的是形参类型顺序是char,int的func函数。
这对重载函数的参数类型顺序不同。
既然说到顺序不同也构成重载函数,那么看下面一段代码:
上图中的代码,上面的func函数的形参是int a,int b,下面的func函数的形参是int b,int a。
它们的顺序是不一样,但是这俩个形参的类型是一样的,都是int类型,所以不能构成重载函数。
构成重载函数时的形参类型顺序不同,形参类型必须是不同的。
知道了什么是函数重载以后,再结合下前面的知识,带有缺省参数的函数可以重载吗?
该段代码中存在着函数重载,上面的函数func是没有参数的,下面的函数func是带有缺省参数的。
调用该函数的时候,由于俩次调用都传进去了int类型的数据,所以调用的都是下面的func函数。
我们知道,带有缺省参数的函数是可以不传实参的,再进行如下调用:
在调用的时候,不传任何实参,可以调用上面的那个没有形参的func,也可以调用下面的带有缺省参数的func,所以会报错,报对重载函数调用不明确的错误。
这个现象称为调用时的二义性。
通过上面俩段程序可以看出,带有缺省参数的函数是可以重载的,但是在调用的时候要明确调用哪个,像上面的例子第一个函数就无法调用,因为无法在调用时明确的调用。
总之,只要在调用的时候不产生二义性,带有缺省参数的函数也是可以重载的。
还记得前面在讲解输入输出的时候,cout函数和cin函数可以自动识别类型吗?到这里你应该明白了,它之所以能够自动识别变量的类型,也是因为函数重载,具体的实现在以后的学习中会给大家讲解。
现在已经知道了C++中有函数重载,那么函数重载是怎么实现的呢?
这里本喵简要的介绍一下,在后面的学习中会有更加深入的介绍。
在本喵的文章程序环境和预处理中曾讲解过,一个C程序运行起来需要经历以下几个阶段:预处理、编译、汇编、链接,C++程序也是这样的。
这样一段C语言程序,在编译阶段会形参符号表,此时函数func会给分配一个地址,我们来看它的汇编语言:
我们写的函数func在编译阶段进行了修饰,变成了_func,并且为该符号分配了地址,在调用的时候,直接进入该地址出执行函数。
C语言中函数名的修饰只是简单的在函数名的前面加一个下划线_。
C++中的函数名也会被修饰,并且它的修饰规则比C语言中复杂很多,我们来看windows下的名字修饰规则:
修饰规则比较复杂,我们只需要了解有这么一个东西就可以。
再看上面我们写过的重载函数的代码:
该代码在编译的过程中,它俩个函数名都经过了修饰,修饰后的符号名分配了不同的地址,来看反汇编中的代码:
可以看到,给俩个函数名Add分配了俩个不同的地址,一个地址表示的是形参是int,int类型的函数,另一个地址表示的是形参是double,double类型的函数。
通过上面的分析我们知道,虽然我们写的重载函数的函数名是一样的,但是经过编译器修饰以后,函数名就不一样了,并且分配了不同的地址,在调用该函数的时候,编译器会自动匹配实参和形参的类型,去相应的地址调用相应的函数。
注意:
在了解了函数重载是怎么调用的以后,本喵提出一个问题,返回类型不同可不可以构成函数重载?
按照编译器将函数名进行修饰这个逻辑是可以的,无非就是再加一些符号表示不同的返回类型,这样就可以实现返回类型不同的函数重载。
但事实上并没有这么干,看下面程序:
虽然构成返回类型不同的重载函数理论上是可行的,但是我们在调用函数的时候是无法规定被调用函数的返回类型的,所以这种重载函数是不存在的。
总的来说,返回类型不同的函数无法构成重载函数的原因是调用时的二义性。
同样无法区分调用的哪个函数,因为调用时并不指定返回类型。
篇幅有限,我们下篇见。