1.C++的发展史
C语言诞生后,很快普及使用,但是随着编程规模增大且越来越复杂,并且需要高度的抽象和建模时,C语言的诸多短板便表现了出来,为了解决软件危机,上世纪八十年代,计算机界提出了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 制定ing
2.C++的重要作用
在开发语言排行榜上,C++几乎稳居前三,可见其泛用性。
(下图为23年6月的)
(下图是2024年8月的)
在工作领域,C++在以下领域有其独到的优势:
从校招中公司岗位的技能要求,以及学长面经总结了解到,公司在校招期间更看重学生的基础,最主要是:语言(至少掌握一门面向对象语言java/C++)、数据结构、操作系统、网络、数据库、设计模式等,而本门C++的授课内容,更注重学生的实践动手能力、工作中的应用以及笔试面试中的技巧,最后达到能够正常工作以及学习即可。
3.C++的基本语法
闲话少说,接下来直接进入C++学习。
下面是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
相比C语言足足多了一倍!(C语言32个,C++63个)
但是这里先不细讲,这些关键字在以后的学习中都会学到的。
3.1 命名空间
什么是命名空间?为什么要有命名空间?
在C/C++中,变量、函数和后面要学到的类都是大量存在的,如果这些变量、函数和类的名称都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
例如:在C语言中
#include<stdio.h>
#include<stdlib.h>
int rand=10;
int main()
{
printf("%d",rand);
return 0;
}
上面这个函数会报错:
它会告诉你,rand是一个函数名字,这里的全局变量rand有命名冲突,因为在库中有了rand这个函数,再次使用rand这个名字定义变量或者函数时,编译器会分不清你到底想使用哪个rand。
可能你觉得问题不大,大不了我换个名字就好了嘛,但是在大型工程项目中,数以MB的大小里面的变量名字可不是简简单单换个名字这么简单
C++为了解决这个问题,引出了命名空间这个玩法(C++兼容C语言的所有语法)
命名空间如何定义呢?
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员
namespace N1 // N1为命名空间的名称
{
// 命名空间中的内容,既可以定义变量,也可以定义函数,也可以定义结构体
int a;
int rand;
int Add(int left, int right)
{
return left + right;
}
struct student
{
char name[];
int age;
}
}
//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;
}
}
如最上面的N1,在这个代码中,rand就定义在命名空间N1中,这和库函数中定义的全局函数rand()不在同一个空间中,所以可以同时存在
命名空间定义后如何使用呢?
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之后并没有使用它,命名空间的使用方法有以下三种:
1.加命名空间名称及作用域限定符:
int main()
{
printf("%d\n", N::a);//::就是作用域限定符
return 0;
}
2.使用using将命名空间中的成员引入:(写项目的时候使用)
using N::b;//将N::b引入全局中
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
3.使用using namespace命名空间名称引入(建日常使用)
using namespce N;//这里相当于把N展开了,也就是把N的成员加入到全局中
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
当然,在命名空间N1中的rand被展开之后相当于加入到全局域,再使用的时候还是会和库里的rand()函数冲突
命名空间的存在奠基了C++能作为创建一个大工程的语言,一个大工程往往会分组安排任务,C++使得每个小组可以使用不同的命名空间,即使命名空间定义相同的名字,编译器也会帮你进行合并,这对于开发者来说十分方便!
以下是一些使用 C++ 开发的知名游戏:
- 《英雄联盟》:这是一款非常受欢迎的多人在线战斗竞技游戏。
- 《使命召唤》系列:著名的第一人称射击游戏。
- 《古墓丽影》系列:动作冒险游戏。
- 《星际争霸》系列:经典的即时战略游戏。
以及 绝地求生****巫师三等
3.2 C++的输入输出
讲到现在,我们甚至还不会用C++写一个“hello world”,这怎么行?上代码:
#include<iostream>
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
代码中,cout就是标准输出(控制台),除此之外还有cin——标准输入(即键盘),使用cout标准输出和cin标准输入时,必须包含< iostream >头文件以及std标准命名空间。(endl就是换行)
需要注意的是:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。
估计细心的同学已经发现了,使用C++进行输入输出的时候不需要增加格式控制,也就是C语言格式化输入输出的%d、%c等。直接cin、cout即可。而且可以一个cin(cout)进行连续输入(输出)
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
3.3缺省参数
缺省,可能单看这个名字看不出来什么意思,但是找到它的英文就知道了:default,其实就是默认。
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
缺省参数分为全缺省和半缺省
全缺省是这样的:
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
Func函数中a,b,c都给了默认值,就叫全缺省
而半缺省长这样:
void Func(int a, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
这里只有b和c给了缺省值,而a没有,就叫半缺省
需要注意的是,半缺省参数只能从右往左依次给出,不能间隔着给,而且缺省参数不能在函数声明和定义中同时出现
//in a.h
void TestFunc(int a = 10);
// in a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
而且缺省值必须是常量或者全局变量。(C语言不支持这个语法)
3.4函数重载
从前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前
者是“谁也赢不了!”,后者是“谁也赢不了!”。这个笑话表明,自然语言中同样的句子可能有不同的含义,而函数重载就和这个类似。
函数重载:是函数的一种特殊情况,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,但是它们的参数类型不同
函数重载还可以是参数数量不同:
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
以及参数顺序不同:
void f(int a, char b)
{
cout << "比较航空航天大学" << endl;
}
void f(char b, int a)
{
cout << "哈尔滨佛学院" << endl;
}
但是形参名字不行:
short Add(short x,short y)
{
return x+y;
}
short Add(short y,short x)
{
return x+y;
}
此外要注意:返回值不同不能构成重载,如:
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
那为什么C语言不支持而C++支持呢?
这就涉及它们整个编译的过程了:在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。在这些过程中,C++文件和C文件的处理不同。
C语言为什么不支持?
首先创建三个文件:func.h func.c main.c 在.h文件中声明两个函数:
int func(int x,int y);
int func(int x,double y);
这三个文件会经历:
1.预处理:头文件展开、宏替换、条件编译以及去掉注释,这个过程结束之后func.h被展开了,main.c func.c成为func.i main.i文件
2.编译:语法检查和生成汇编代码func.i和main.i变成了func.s main.s(文件内容是汇编代码)
3.汇编:将汇编代码转换成二进制码,以便机器能够读懂,此时变成func.o main.o
4.链接(最关键):链接时,.o文件会合并在一起,而且还需找一些只给了声明的函数
的函数地址,而每一个.o文件都有一个符号表,符号表中存放函数的地址,当main文件要调用这个函数时,会去符号表中找函数的地址
而符号表中两个func函数的地址,编译器不知道应该调用哪个,所以c程序不支持函数重载。
那C++为什么支持呢?
相比起C程序而言,C++新增了一个函数名修饰规则来支持函数重载,这个规则就是将函数的参数带入符号表,所以参数的类型,数量,顺序不同,代表的是不同的函数,找地址时就不会出错!
将C++代码转到反汇编,我们可以看到:
函数参数的类型,数量,顺序不同,那么对应在符号表中的名字就不一样,main文件再去找函数地址时就不会冲突。这个命名规则C++标准并没有具体规定,由每个编译器自己决定,如VS2022就与g++不同(VS2022的命名规则比较诡异),但是一定可以保证的是,参数不同的函数名字不同
对比C语言,c程序符号表中只有一个函数名,函数参数没有参与进来,所以C程序不支持相同函数名的函数。
那么这时候就会有同学问了,如果在符号表命名规则中加入返回值不就可以让不同的返回值支持函数重载了吗?
答案是不能!
因为在调用的时候并没有返回值,比如下面这两个函数:
void add(double x, double y)
{
return ;
}
double add(double x, double y)
{
return x+y;
}
double a=0.5,b=0.5;
在调用的时候,我们只有add(a,b)
,那么即使符号表的问题解决了,调用规则摆在这,还是无法确定应该调用符号表中的哪个函数。(除非你把调用规则也改掉,让它在调用的时候带上返回值一起)
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
此时就会链接时报错:error LNK2019: 无法解析的外部符号_Add,该符号在函数 _main 中被引用
因为函数按照C语言的规则来命名。
下面两个函数构成函数重载吗?
void TestFunc(int a = 10)
{
cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
cout<<"void TestFunc(int)"<<endl;
}
持续更新ing…