1、背景知识
1.1 什么是C++
C语言是结构化和模块化的语言,长用于处理较小规模的程序;对于规模较大、问题复杂的程序,则需要高度的抽象和建模,此时C语言不合适处理这类问题。为了解决此类影响软件的问题,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言的渊源关系,命名为C++。因此C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
1.2 C++的发展历史
1979年,贝尔实验室的本贾尼等人试图分析Unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
C++创始人:本贾尼
下图为C++在不同时间的历史版本:
1.3 C++的使用广泛度
下图数据来源于TIOBE编程语言社区2023年1月4日的最新排行榜,在30多年的发展中,C/C++排名一直稳居前五。
由此可以看出,C/C++、Java是目前比较主流的程序语言,尤其是C++语言掌握者适合在操作系统及大型软件开发、服务器端开发、游戏开发、嵌入式和物联网、数字图像处理、人工智能、分布式应用等领域工作。
2、基础语法
2.1 "::"域作用限定符
域作用限定符表示作用域,和所属关系。下列代码中,cout
、endl
属于std, 而std是C++标准库中函数或者对象
int main()
{
std::cout << "Hello World" << std::endl;
std::cout << "Hello World" << "\n";
return 0;
}
endl
表示换行,类似于C语言中的’\n’,cout
表示类似于C语言中printf的格式化输出;
调试结果:
2.2 命名空间
在C/C++中,大量存在着变量、函数、类,这些都存在于全局作用域中,会导致名字冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或者名字污染,namespace
关键字的出现就是针对这种问题。
下图中,在只包含标准输入输出库stdio
中,全局变量rand
能够正常使用:
但加入了标准库函数头文件后,全局变量rand就被“重定义”报错了,这是因为在stdlib
中又随机函数rand,与下图的全局变量rand命名冲突了。
例如,下列代码中,全局域和局部域中整型变量a的名字都相同,C语言中局部域无法调用全局域变量a。
int a = 1;
void f1()
{
int a = 0;
printf("%d\n", a);
printf("%d\n", a);
}
void f2()
{
int a = 1;
}
int main()
{
printf("%d\n", a);
f1();
return 0;
}
调试结果:
C++在局部域的输出函数中添加域作用限定符,使得局部域中也能调用全局域的变量。
上图中,::的前面空格表示在全局中查找,不能在其他函数中查找。
2.2.1 命名空间的定义
在C++中,定义命名空间需要使用关键字namespace
,后面跟着命名空间的名字,然后接一对花括号{},{}中为命名空间的成员。
- 一般命名空间的定义事例:
namespace joes
{
int rand = 10;
int Add(int left,int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
}
}
- 命名空间的嵌套事例:
namespace P1
{
int a;
int b;
int Add(int left,int right)
{
return left + right;
}
namespace P2
{
int c;
int cp;
int Sub(int left,int right)
{
return left - right;
}
}
}
- 命名空间的合并:
AA.h
头文件
namespace joes
{
int Add(int* a,int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
}
BB.h
头文件
namespace joes
{
int Add(int& x,int& y)
{
int tmp = x;
x = y;
y = tmp;
}
}
同一个工程中允许存在多个相同名称的命名空间,如上例中的joes
,编译器最后会合成同一个命名空间中joes
。
2.2.2 命名空间的使用
主函数:
int main()
{
struct AList::Node node1;
struct BQueue::Node node2;
AList::min++;
BQueue::min++;
return 0;
}
List.h
头文件:
namespace AList
{
struct Node
{
struct Node* prev;
struct Node* next;
int val;
};
int min = 1;
}
Queue.h
头文件:
namespace BQueue
{
struct Node
{
struct Node* next;
int val;
};
struct Queue
{
struct Node* head;
struct Node* tail;
};
int min = 0;
}
2.3 C++的输入与输出
当我们在学C语言时,编写的第一个程序为输出"Hello World":
#include<stdio.h>//C语言的标准输入输出库
int main()
{
printf("Hello Word\n");
return 0;
}
而Python亦较为简洁:
print("Hello World")
那么C++是如何实现的?
#include<iostream>
using namespace std;
int main()
{
cout << "Hello World" << endl;
return 0;
}
注意:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含头文件以及命名空间使用方法使用std;
- **<<是流插入运算符,>>**是流提取运算符;
- C++的输入输出不需要像printf及scanf那样手动控制格式,C++的cout及cin能够自动识别变量类型;
- 在日常学习中,建议直接using namespace std即可;但using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +using std::cout展开常用的库对象/类型等方式。
2.4 缺省参数
2.4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a=0)
{
cout << a << endl;
}
int main()
{
Func();//没有传参数时,使用参数的默认值0
Func(1);//传参数时,使用指定的实参1
return 0;
}
调试结果:
2.4.2 缺省参数分类
- 全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl;
cout << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
Func();
return 0;
}
调试结果:
注意:使用缺省值必须从右往左连续使用。
- 半缺省参数
void Func(int a, int b = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl;
cout << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
return 0;
}
调试结果:
注意:半缺省必须从右往左连续缺省。
2.4.3 缺省参数的实际应用
例如数据结构中,栈的建立,具体详见我的上篇博文【数据结构】超详细——动态栈
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps,int defaultCapacity = 4)
{
ps->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->top = 0;
ps->capacity = defaultCapacity;
}
int main()
{
Stack st1;//最多要存100个数据时
StackInit(&st1, 100);
Stack st2;//不知道有多少数据时
StackInit(&st2);
return 0;
}
此外,缺省参数不能在函数声明和定义中同时出现。
void Func(int a = 20)
{
cout << "a= " << a << endl;
}
int main()
{
Func();
return 0;
}
下面为头文件:
void Func(int a = 10);
调试结果:
出现错误是由于程序的头文件中,函数Func声明和C++程序中Func的定义都使用了缺省参数。修改声明后:
void Func(int a);
调试结果:
修改后程序正常执行,亦可以修改函数的定义文件,删去缺省参数。
2.5 函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载。
2.5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一个作用域中,声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
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;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
return 0;
}
调试结果:
类型顺序不同:
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout<<endl;
}
void f(char b, int a)
{
cout << "f(char b,int a)" << endl;
cout << "b=" << b << endl;
cout << "a=" << a << endl;
cout << endl;
}
int main()
{
f(10, 'j');
f('u', 20);
return 0;
}
调试结果:
上例中,函数名相同,形式参数列表的类型顺序不同以及类型不同。
2.5.2 C++支持函数重载的原理–名字修饰(name Mangling)
C/C++语言中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、 链接。
预处理:文件展开,条件编译,宏替换,去注释等;
编译:C++语言汇编语言;
汇编:将编写的代码翻译成二进制目标文件;
链接:将形成的.obj文件和库文件合并,形成可执行程序。
根据上图,编写具如下程序:
对程序进行预处理:
对程序进行编译:
对程序进行汇编:
对程序进行连接:
调试结果:
采用C++语言编写的test.cpp程序,g++编译的结果可知,函数名的修饰发生变化,编译器将函数参数类型信息添加到修改后的名字中,g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
而函数重载时,g++编译后函数修饰名具体为:
而在linux环境中,采用gcc编译C语言程序后,函数名字的修饰没有发生改变:
注意:
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
2.6 引用
2.6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例如:林允儿,人称“允儿”,江湖人称“小鹿”。
林允儿
类型& 引用变量名(对象名)= 引用实体
例如下列代码中,变量k是i的别名,m亦是i的别名,而n是k的别名:
int main()
{
int i = 0;
int& k = i;//引用
int j = i;
cout <<"i的地址:"<< & i << endl;
cout <<"j的地址:"<< & j << endl;
cout <<"k的地址:"<< & k << endl;
++k;
++j;
int& m = i;
int& n = k;
++n;
return 0;
}
变量i、j、k的地址为:
由此可知,引用不在内存中开辟新的空间。此外,引用类型必须和引用实体是同种类型的。
2.6.2 引用特性
- 引用在定义时必须初始化;
- 一个变量可以有多个引用;
- 引用一旦引用一个实体,再不能引用其他实体。
int main()
{
int i = 11;
int* p = &i;
int*& rp = p;
return 0;
}
若引用定义时未初始化,则会出现下图的报错提醒:
在上述代码行中,i被p和rp引用。
2.6.3 常引用
#include<iostream>
using std::cout;
using std::endl;
void TestConst()
{
const int a = 10;
const int& ra = a;
const int& b = 10;
}
int main()
{
TestConst();
return 0;
}
调试结果:
由此可见,代码执行正常。但常见下面错误:
void TestConst()
{
const int a = 10;
int& ra = a;
}
int main()
{
TestConst();
return 0;
}
调试结果:
这是由于变量a是常量,引用时遗落了常量类型关键字const
。