1、C++发展史:1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩 展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes(所以c++是可以兼容c的)。语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本(只介绍本人认为需要了解的几个版本):
(1)c with classes:在C的基础上新增了类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等。
(2)c++98:C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)。
(3)c++03:C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性。
(4)c++11:增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等。
(5)c++14:对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回 值类型推导,二进制字面常量等。
2、C++关键字(c++98):关键字(keyword)又称保留字,是整个语言范围内预先保留的标识符。每个C++关键字都有特殊的含义。经过预处理后,关键字从预处理记号(preprocessing-token)中区别出来,剩下的标识符作为记号(token),用于声明对象、函数、类型、命名空间等。不能声明与关键字同名的标识符。
3、命名空间:在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
(1)命名空间的定义:定义命名空间,需要使用到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 //N3定义在N2里
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间
// N1与上面定义的N1重名,进行编译后会合成为同一个命名空间中(内容扩展了)。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
(2)命名空间的使用:我们先定义一个简单的命名空间
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(void)
{
printf("a=%d\n",a);
return 0;
}
当我们直接使用命名空间里定义的变量a时,编译就会报错,无法识别a。所以接下来接受命名空间使用的三种方式:
●加命名空间名称及作用域限定符
int main(void)
{
printf("%d\n", N::a);
return 0;
}
●使用using将命名空间中成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
●使用using namespace 命名空间名称引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
Sub(20, 10);
return 0;
}
4、C++的输入&输出:使用cin和cout作为输入输出。
(1)使用cout输出和cin输入时,必须包含< iostream >头文件
注意:后缀为.h的头文件 C++标准已经不支持了,早期的实现将标准库功能定义在全局域中,声明在.h后缀的头文 件中。C++为了和C区分,也为了正确使用命名空间,规定头文件不使用.h
(2)cout和cin包含在std标准命名空间中,使用时必须包含该命名空间 注意:旧编译器(vc6.0)中可能含有
(3)C++中使用cout进行输出(到控制台),使用cin进行输入(来源自控制台),C的方式可继续使用
(4)使用C++输入输出更方便,不需增加数据格式控制,比如:整形--%d,字符--%c
#include using namespace std;
int main()
{
int a;
double b;
char c;
cin>>a;
cin>>b>>c;
cout<
5、缺省参数:缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认 值,否则使用指定的实参。
void TestFunc(int a = 0)
{
cout<
缺省参数可以分为:全缺省参数和半缺省参数(也叫局部缺省)
(1)全缺省参数:申明或定义函数时给所有的参数都指定默认值。
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<
(2)半缺省参数:申明或定义函数时指定部分参数的默认值。
void TestFunc(int a, int b = 10, int c = 20)
{
cout<<"a = "<
注意:1. 半缺省参数必须从右往左依次来提供,不能间隔着给出 2. 缺省参数不能同时在函数声明和定义中出现 3.缺省值必须是常量或者全局变量 4. C语言不支持(编译器不支持)
●当声明与定义分离时,只能在声明中出现。在定义中会出现编译错误。
//pub.h
int Add(int a , int b = 10);
//pub.cpp
int Add(int a , int b)
{
return a+b;
}
6、函数重载:是函数的一种特殊情况,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) 名字修饰(name Mangling)
我们知道,在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。Name Mangling是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个 函数,将函数通过一定算法,重新修饰为一个全局唯一的名称。
●C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。比如,对于以下代码,在后链接时就会出 错:
int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
(编译器报错:error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。)
上述Add函数只给了声明没有给定义,因此在链接时就会报错,提示:在main函数中引用的Add函数找不到函数体。从报错结果中可以看到,C语言只是在函数名前添加了下划线。因此当工程中存在相同函数名的函数,由于没有做过多的区分就会产生冲突。
●由于C++要支持函数重载,命名空间等,使得其修饰规则比较复杂,不同的编译器在底下实现的方式可能都有差 异。
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
在vs下,对上述代码进行编译链接,后编译器报错:
error LNK2019: 无法解析的外部符号 "double cdecl Add(double,double)" (?Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在终的名字中,就可保证名字在底层的全局唯一性。
(2) extern “C” :有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
链接时报错:error LNK2019: 无法解析的外部符号_Add,该符号在函数 _main 中被引用。
7、引用:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a; //<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注意:引用类型必须和引用实体是同种类型的
(1)引用特性:
●引用在定义时必须初始化
●一个变量可以有多个引用
●引用一旦引用一个实体,再不能引用其他实
void TestRef()
{
int a = 10; //
int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra); //打印时,这三个地址相同
}
(2)常引用:
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
const int b = 10;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d; //可以通过,相当于取d的值,再做Int强转。实际应用中不建议
}
(3)使用场景:
●作形参
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
●作返回值
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
注意:不能返回栈空间上的引用
(4) 传值、传地址、传引用效率比较 :
#include
#include
using namespace std;
typedef struct _A{ //定义一个结构用于测试
int buf[1000000];
}A;
void Test(A& a){
a.buf[0] = 0;
a.buf[1] = 1;
}
void TestPtr(A *a){
a->buf[0] = 0;
a->buf[1] = 1;
}
int main(void){
int i = 0;
//传引用
long begin = GetTickCount(); //记录执行代码前的时间
A a;
for (;i < 1000000;i++){
Test(a);
}
long end = GetTickCount(); //记录执行完Test时的时间
cout<<"传引用所需的时间"<
通过上述代码的比较,发现引用和指针在传参上效率几乎相同。
(5) 引用和指针的区别:
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
我们来看下引用和指针的汇编代码对比:
我们可以看出,在底层实现上引用实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点:
a. 引用在定义时必须初始化,指针没有要求
b. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
c. 没有NULL引用,但有NULL指针
d. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
e. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
f. 有多级指针,但是没有多级引用
g. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
h. 引用比指针使用起来相对更安全
8、内联函数:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联 函数提升程序运行的效率。
(1)特性:
● inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用 作为内联函数。
● inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编 译器优化时会忽略掉内联。
(2)宏的优缺点:
优点:
●增强代码的复用性。 ●提高性能
缺点:
●不方便调试宏。(因为预编译阶段进行了替换)
●导致代码可读性差,可维护性差,容易误用。 ●没有类型安全的检
(3)c++可以替代宏的技术:
●常量定义-----换用const
●函数定义-----换用内联函数
●类型重定义-----换用 typedef
注意: 如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline )函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。在函数的声明或函数的定义两者之一作inline声明即可。