前言
在经历了一年的c语言打怪升级,笔者终于要进入c++的学习了,这将又是一场漫长的打怪升级过程。
首先要带大家了解一下C++语言以便我们后续的学习:
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式 等。熟悉C语言之后,对C++学习有一定的帮助。
C++补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
本文笔者将讲述C++在c语言的基础上进行了哪些添加,带有c语言基础的同学们丝滑过度到C++的世界。
C++的发展历史
学习一个新语言首先我们要搞清楚他的由来背景:
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C
语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with
classes。
C with classes就是C++的前身,这个新创立的语言完全建立在c语言的基础上,将其进行了拓展,解决了c语言在开发过程中的很多问题!
C++也和java等主流语言类似,经历过几十年的版本迭代越来越完善,和我们打怪升级学习一个新技能一样,C++的发展也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本。
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
… | … |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
… | … |
C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所有大家不用追求最
新,重点将C++98和C++11掌握好,等工作后,随着对C++理解不断加深,有时间可以去琢磨下更
新的特性。
所以我们主要学习的版本就是C++98和C++11
下面开始进行我们的正式学习过程啦!
命名空间
在c语言中如果我们要进行一个大型项目的编写,一个团队几人分别负责不同的模块,那么就容易出现一个问题——两个人写的模块之间会有命名冲突问题。这时候就需要其中一个人去修改他的名称。无疑非常浪费时间和精力。所以C++的祖师爷本贾尼可能也深受其折磨,所以在C++中引入了一个概念——命名空间。
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存
在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,
以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
那么命名空间该如何定义与使用呢?
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}
中即为命名空间的成员。
namespace my_space
{
//我的命名空间
int ADD(int x, int y)
{
return x + y;
}
}
int ADD(int x, int y)
{
return x + y;
}
int main()
{
ADD(1, 2);
return 0;
}
让我分析一下上面的代码,如果按照c语言的思路上述代码编译时就会报函数名重复定义的错误。可是在C++中我们将同名的函数放到了my_space的命名空间时,编译器在使用者不调用这个命名空间时就不会去其中内部查找定义,所以我们可以运行。
命名空间相当于一面墙将其中的ADD函数封了起来,所以我们默认调用的是墙外面的ADD,即为第二个ADD函数,那么如何打破这面“墙”,去墙内找呢?
命名空间的使用有三种方式:
- 加命名空间名称及作用域限定符
int main()
{
//ADD前添加my_space::
my_space::ADD(1, 2);
return 0;
}
我们在ADD前添加my_space::的意思就是为ADD函数指定作用域,让编译器只在my_space的作用域内寻找ADD函数。
在提及剩下两个方法之前我要插播一个概念:即C++标准库iostream与其命名空间std。C++标准库和c语言的库函数的内容相似,不过其将其的函数定义都放入了命名空间std内大家了解这个概念即可。
那么如果我想用C++的标准库打印一个东西该怎么办呢?
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
其中的cout大家就可以看作标准库内的一个函数,我们现在不用了解其中的原理,调用时我们用了命名空间的的第一种使用方法std::。
这时候我们就会想如果我一个项目多次使用了cout这个函数,每一次都打一边std::太麻烦了!所以就有了第二种使用方法:
- 使用using将命名空间中某个成员引入
using std::cout;
using std::endl;
int main()
{
cout << "hello world" << endl;
return 0;
}
这时候当编译器看到cout与endl时,就会自动在std这个命名空间内查找!
有伙伴就要说了,我还是觉得麻烦,我平常就写个练习也不写项目,我想调用标准库函数时它都帮我去std内查找怎么办?没有关系我们还有第三种方法:
- 使用using namespace 命名空间名称 引入
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
这时候我们就相当于把std整面墙打破了,所有函数自动会先在std这个命名空间中查找一般,再会到空间外查找。
当然在实际写项目时,非常不建议大家使用第二种和第三种方法,因为这样的话命名空间存在的意义就消失了。
缺省函数
在实际写代码时我们可能会碰到一个函数的参数大部分情况下都会是同一个数,但是仍有小概率要传参其它的值,所以C++引入了缺省函数的概念。
在定义函数的时候我们可以对形参进行赋值:
namespace my_space
{
//我的命名空间
int ADD(int x, int y)
{
return x + y;
}
void fuc(int a = 0)
{
cout << a << endl;
}
}
int main()
{
my_space::fuc();
return 0;
}
这时候fuc函数的参数a即为缺省参数,调用时可以传空值,打印出来的结果即为0。
int main()
{
my_space::fuc(5);
return 0;
}
如果我们不传空值那么打印结果为4。
上面所有参数都缺省的叫做全缺省,也有部分参数进行缺省的半缺省:
在半缺省时为了防止编译器产生歧义,我们规定了缺省参数只能在函数的最后定义,且缺省参数要在一起。所以定义半缺省函数时半缺省参数必须从右往左依次来给出,不能间隔着给。
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
我们也都知道,在实际项目中一个函数需要定于与声明,那么缺省参数要在两个地方都要有吗?答案是否定的。语法为了避免定义与声明时程序员乱写,在定义与声明中的缺省值不同导致歧义所以规定只能出现在一处,一般我们将其写在声明内。
函数的重载
在日常生活中有很多同名的人,但是我们仍然可以通过同名人的不同相貌,身份特征去区分它们。同样在C++语法中C++允许在同一作用域内声明几个功能类似的同名函数——这些同名的函数就叫函数的重载。而如何区分同名的函数呢?语法规定了函数的参数类型,个数需要不同。
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
函数的重载仅限于函数的参数不同。仅返回值不同,参数相同的同名函数不构成重载
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
这时a和ra都是同一片地址空间ra仅是a的别名,所以打印的值都为10。
引用的特折:
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
即为引用初始化以后便无法改变了。
常引用:
我们可以对常变量进行引用,但是在引用时我们要注意权限不能被放大只能被减小,所以对const int类型的常变量进行引用时我们不能用int&进行引用,但是可以对int类型进行const int&引用:
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 int& rd = d;这个语句大家可能觉得非常离谱,这里要详解一下:因为rd与d的类型不同,会发生隐式类型转换,而d在隐式类型转换时会产生一个临时变量,而这个临时变量具有常性质。
使用场景:
- 做参数
在c语言时我们要写一个交换函数时要传入两个数的指针,通过解引用的操作进行交换。而在C++中我们可以使用引用作为参数直接对两个数进行交换:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
- 作为返回值
int n = 0;
int& Count()
{
n++;
// ...
return n;
}
int main()
{
Count()++;
cout << n <<endl;
return 0;
}
此时输出的结果为2。
下面代码输出什么结果?为什么?
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;
}
此时输出的结果为7,这种情况使用引用返回非常危险
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
内联函数
内敛函数是在c语言宏函数的基础上建立起来的,但相较于宏函数使用内联函数编写更加便捷与安全!
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
不使用内联函数时:
使用内联函数时:
通过反汇编我们可以看出:使用内敛函数减少了栈的开销。
有些同学的vs需要设置才可以实现,实现方法如下:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,以下给出vs2013的设置方式)
那么是不是所有函数都使用内联的方法就好呢?inline是一种以空间换时间的做法,大量使用内联函数展开会造成目标文件变大。一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰。