目录
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++关键字:C++总计有63个关键字,而C语言有32个 :
⭐总结:C++相对来说更应该是C语言的进化版。
- 在我们写C++时首先会在前两行写下面代码:
#include using namespace std;
- 第一行的头文件等价于我们C语言学到的#include
,它是用来跟我们的控制台输入和输出的。而第二行的 namespace就是我们要接触C++的第一个关键字,它就是命名空间。- 命名空间的作用
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
在我们谈namespace之前我们先来看一下C语言的命名冲突问题:
#include //命名冲突 int rand = 0; int main() { printf("%d", rand); return 0; }对于如上代码我们定义了全局变量rand,并且代码可以正常运行没有错误,但如果我们包含上头文件#include
呢??我们都知道 C语言中存在一个库函数rand,其头文件就为#include。 下面我们运行下看看结果:这里很明显是发生了命名冲突,我们定义的全局变量rand和库里的rand函数发生了冲突。
- 为了解决此类命名冲突问题,我们在C++中引入了命名空间namespace。
定义命名空间我们需要用到namespace关键字,后面跟命名空间的名字,然后接一对{ },{ }中即为命名空间的成员。
在同一个作用域中不能出现两个相同变量,此时的rand被关在n1的命名空间域里了,跟其它变量进行了隔离。所以在stdlib.h头文件展开时并不会发生命名冲突。此时rand的打印均是库函数里rand的地址,rand就是一个函数指针,打印的就是地址。
我们再看下面的一个例子:
- 此段代码更充分的体现了加上命名空间,不仅可以避免命名冲突,而且还告诉我们,此时再访问变量a、b、c,均是在全局域里访问的,而xzy这个命名空间域里的变量与全局域建立了一道围墙,互不干扰。不过这里a和b依旧是全局变量,命名空间不影响生命周期。
- 命名空间中可以定义变量,函数,自定义类型
namespace N1 { int a; //变量 int Add(int left, int right) //函数 { return left + right; } struct ListNode //自定义类型 { int val; struct ListNode* next; }; }
- 命名空间可以嵌套
namespace N2 { int a; //变量 int Add(int left, int right) //函数 { return left + right; } namespace N3 { int c; int d; int Sub(int left, int right) { return left - right; } } }
- 同个工程允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1 { int a; //变量 int Add(int left, int right) //函数 { return left + right; } } namespace N1 { int Mul(int left, int right) { return left * right; } }
我们知道在C语言中存在局部优先原则:
第一次打印出1是因为局部优先原则,第二次打印是引用了域作用限定符 ( :: ),此时访问的a,就是全局域里的a。
我们定义下面的命名空间:
namespace n1 { int f = 0; int rand = 0; }我们该如何访问命名空间域里的内容呢??我们有三种方法:
- 加命名空间名称及作用域限定符"::"
- 使用using namespace 命名空间名称全部展开
- 使用using将命名空间中成员部分展开。
- 1、加命名空间名称及作用域限定符"::"
为了防止定义相同的变量或类型,我们可以定义多个命名空间来避免
namespace ret { struct ListNode { int val; struct ListNode* next; }; } namespace tmp { struct ListNode { int val; struct ListNode* next; }; struct QueueNode { int val; struct QueueNode* next; }; }当我们使用它们时,如下:
int main() { struct ret::ListNode* n1 = NULL; struct tmp::ListNode* n2 = NULL; return 0; }针对命名空间的嵌套,如下:
我们可以这样进行访问:
int main() { struct n1::List::Node* n1; //访问List.h文件中的Node struct n1::Queue::Node* n2; //访问Queue.h文件中的Node return 0; }
- 2、使用using namespace命名空间名称全部展开
using namespace n1;
int main() { struct List::Node* n1; //访问List.h文件中的Node struct Queue::Node* n2; //访问Queue.h文件中的Node return 0; }当然我们还可以再拆一层:
using namespace n1; using namespace List; int main() { struct Node* n1; //访问List.h文件中的Node struct Queue::Node* n2; //访问Queue.h文件中的Node return 0; }展开时需要注意的是先展开n1,再展开List,顺序不能颠倒。这种方法虽然简便,但也存在一定的风险,命名空间全部释放又重新造成命名冲突。
所以针对某些特定会出现命名冲突问题,我们需要单独进行讨论:
由此我们得知:全部展开并不好,我们需要按需索取,用什么展开什么,由此引出第三种使用方法:
- 2、使用using将命名空间中成员展开
针对上述代码,我们只将f放出来
namespace n1 { int f = 0; int rand = 0; } using namespace n1; int main() { f += 2; printf("%d\n", f); f += 2; printf("%d\n", f); f += 2; printf("%d\n", f); n1::rand += 2; printf("%d\n", n1::rand); }
- 学到这里,我们来看一下C++的标准库命名空间:
#include using namespace std; //std是封C++库的命名空间 int main() { cout << "hello world" << endl; //hello world return 0; }如果我们这行代码:
using namespace std;
我们想要输出hello world就需要这样做:
#include int main() { std::cout << "hello world" << std::endl; return 0; }或者这样做:
#include using std::cout; int main() { cout << "hello world" << std::endl; return 0; }
C语言中,我们都清楚输入用scanf,输出用printf,可是在C++中,我们同样可以用C语言的,不过C++也独有一套输入cin输出cout。
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。
// >> 流提取运算符 cin >> a; // << 流插入运算符 cout << a;C++里的输入输出流可以自动识别类型,不需要像C语言一样不需增加数据格式控制,比如:整形--%d,字符--%c
⭐:endl是换行符,等价于C语言中的'\n'。
最后我们用hello world来结束C++的输入输出
#include using namespace std; int main() { cout << "hello world" << endl; }
缺省参数(默认参数)是声明或定义函数时为函数的参数指定的一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
根据上述代码,我们得知,如若你不传参数,那我就用缺省参数,函数默认的参数值,这里是0。如若你传了参数,那就用你自己的。
缺省参数分为两类:全缺省和半缺省,下面我们进行细谈。
我们看下面代码:
void TestFunc(int a = 10, int b = 20, int c = 30) { cout << "a=" << a << endl; cout << "b=" << b << endl; cout << "c=" << c << endl << endl; }根据这段代码,我们这个缺省参数有3个,那么我在调用函数的时候,就有4种调用方式:
⚠:在传参数的时候我们要按照顺序来传,不能第一个没传就传第二个。
//错误:TestFunc( ,1, ) //err
半缺省参数就是既定给出的参数少了一些,看如下代码:
void TestFunc(int a, int b = 20, int c = 30) { cout << "a=" << a << endl; cout << "b=" << b << endl; cout << "c=" << c << endl << endl; }上述代码就已经很明确了,我在调用函数时,传的参数至少是一个,有以下三种调用方式:
int main() { TestFunc(1); //全用默认的 TestFunc(1,2); //只有C用默认的 TestFunc(1, 2, 3); //全部自己传参 return 0; }⚠:半缺省参数必须从右往左依次给出默认参数,不能间隔着给
//错误 void TestFunc(int a=10,int b,int c=30) //err { //......错误 }
我们以栈为例:
struct Stack { int* a; int size; int capacity; }; //以前: void StackInit(struct Stack* ps) { //……} //现在:缺省参数版 void StackInit(struct Stack* ps, int n = 4) { assert(ps); ps->a = (int*)malloc(sizeof(int) * n); ps->size = 0; ps->capacity = n; }这样我们在初始化栈的时候就可以直接使用缺省参数:
int main() { Stack st; StackInit(&st); StackInit(&st, 100); return 0; }
- 缺省参数不能在函数声明和定义中同时出现。
假设我们在Queue.h文件中声明如下:
在Queue.cpp文件中定义此函数:
接下来我们在test.cpp中进行编译:
很明显这样不行。
- 现在我们声明给缺省参数,定义不给:
答案是可以的!
- 如果声明不给定义给也会报错,这个就不演示了,大家可以自己动手演示一遍。
⭐综上:
- 缺省参数不能在声明和定义中同时出现,防止出现不同的赋值导致奇异。
- 缺省参数不能声明不给定义给,会出现链接错误。
- 缺省参数可以声明给定义不给。
函数重载:是函数的一种特殊情况。C语言不支持函数重载,而C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
- 参数类型不同
- 参数个数不同
- 顺序不同
⚠: 仅仅修改函数的返回类型不是函数重载,因为无法区别你要调用的是谁
🤔:为何C语言不支持函数重载,反倒C++可以呢??
- C语言为何不能支持函数重载,而C++却可以??其实这涉及程序的编译链接,接下来我将在linux环境下展示其具体过程。
⭐:gcc—编译C语言 g++—编译C++
首先,我们在Linux环境下创建3个目录:f.h、f.c、test.c。来分别进行声明、定义、实现。
注意后缀,都是以.c命名,这说明以下操作是在C语言的情况下进行的。
- f.h文件
- f.c文件
- test.c文件
接下来我们对上述代码进行编译运行,用gcc编译对它生成tc可执行程序,用g++编译对它生成tcpp可执行程序,并且两个文件编译运行均没错误
接下来我们在原有文件的基础上再添加一个函数来确保其是函数重载
- f.h文件
- f.c文件
- test.c文件
接下来我们分别使用g++和gcc进行编译:
使用g++时可以编译成功,可使用gcc时却编译失败,这就足以说明C语言是不支持函数重载的,想要搞清楚其原因,我们就需要明白程序的编译链接。
我们针对上述的三个文件:f.h、f.c、test.c。展开讨论:
程序的编译链接分为四大过程:
- 预处理—头文件展开、宏替换、条件编译、去掉注释。预处理后生成f.i和test.i文件。
- 编译—检查语法,生成汇编代码。编译后生成f.s和test.s文件。
- 汇编—把汇编代码转换成二进制的机器码。汇编后生成f.o和test.o文件。
- 链接—段表、符号表的合并和符号表的重定位。通俗讲就是找调用函数的地址,链接对应上,合并到一起。
⭐重点是第四阶段链接:
- 在我们代码编译后会生成符号表(记录的是函数的定义和函数地址的映射)以及函数调用指令。所以在我们的f.o和test.o里面有函数调用指令和符号表。
在这里生成了main函数的指令,其中需要调用函数f,但因为并不知道其确切地址,只是有声名,我们只知道有这个函数,所以我们先用?表示。然后进入链接,找调用函数的地址链接对应上并且合并到一起,随后就在编译形成的符号表里寻找与main函数指令相对应的函数名,并找到其地址。
- C语言函数名修饰
我们采用如下指令进行编译:
得到结果:
- 我们可以看到C语言是直接以函数名命名,没有任何其它的修饰,这么做也就注定造成了出现多个相同函数名的时候,在链接时call不知道链接哪个,因为函数名都是一样的,找不到其地址,这也就说明了C语言不支持函数重载。
- C++函数名修饰
我们采用如下指令进行编译:
得到结果:
我们观察C++的函数汇编指令,观察到两个不同函数的函数名修饰样式:
- 一个是<_Z1fid>
- 一个是<_Z1fdi>
通过观察我们能够发现C++函数名的修饰规则:
- _Z+函数名长度+函数名+类型首字母
所以C++编译后生成的符号表里以及链接时函数调用指令应该是这个样子:
⭐: C++在链接的过程中,call找的就是其修饰后的函数名,函数名不同,自然不会出错,这就是C++支持函数重载的核心所在,而C语言的函数命名规则是根据函数名设定的,函数名相同的话,链接就会出错,找不到确切地址,自然不会支持函数重载。
我们以之前写过的栈为例。
首先我们把之前写好的栈拷贝到新的项目里来:
此时进行编译运行是不会通过的(没有调用main函数接口),接下来我们将其改成静态库试试:
- 1、右键属性
- 2、单击配置类型更改为静态库
编译运行,生成.lib后缀的文件
现在有一个C++的项目,想要调用刚才C语言的静态库,如下:
以括号匹配这道题为例,解决这道题需要用到栈的思想。
现在我们的C++项目已经创建完毕,现在到了调用的时刻,看下面:
首先,最基本的是我要先包含头文件,但是自己创建的这个项目工程里并没有栈.h文件,所以要在文件目录的上层寻找:
此时编译并没有问题,但是运行会报一堆错误:
为什么会出现运行错误呢?
就是因为我们对C配置了静态库,现在对这个C++工程也要配置下,如下:
首先:
其次:
随后,把附加库目录生成的,lib文件名字放到如图所示位置:
此时链接器链接就会链接到它的静态库
此时我们再编译运行,发现依旧出错
当我们把Stack.c的后缀改为cpp时
此时我们再运行看看:
此时就编译运行通过了,为什么把Stack_C的后缀改为.cpp就可以通过呢?
- 这里就牵扯到上文谈到的C++和C在汇编中不同的函数名修饰规则了,在C语言中,只有函数名,可是C++有函数类型个数什么的,用原先.c后缀的话就会导致链接出错,改为后缀.cpp就实现了C++调C++,就没有问题了。
⭐:可现在我们要做的是用C++调用C,那我们该如何操作呢??
此时就需要用到我们的extern"C"。我们知道C++是兼容C的,它认识C语言的命名规则
加上extern"C"后,我Stack.h声明的这些东西,都会展开在extern"C"这个括号里面,核心作用就是告诉编译器,extern "C" 声明的函数,是C库,要用C的方式链接调用,此时我们再运行下看看:
此时就没有问题了,成功实现了C++调用C。
实现了C++调用C,接下我们实现下C调C++。
和创建C静态库一样,我们还是新创建一个项目,还是以栈为例,把项目配置成静态库。
接下来我们创建一个C的项目:
首先还是要先包含头文件,依旧是去上层目录寻找:
此时你会发现,编译没有错误,其实这里的编译是有问题的,这里链接的其实是C的库,这个时候是C调C,这里我们需要重新配置下链接库目录。
此时编译运行依旧报错,会发生链接错误。
- 这里链接错误的原因依旧是跟C++的函数名修饰规则有关,C语言是没有修饰的,而C++是修饰过的,这里当然会出现链接错误。这里的解决方案也并不是像C++那样仅仅加个extern"C"就可以解决的,因为extern"C"只是在C++支持,C语言不支持。那么具体怎样操作呢??我们只需要在extern "C"的基础上加上条件编译即可解决。
这里我们针对Stack_CPP静态库进行修改。
- 法一:在Stack.h文件声明的所有函数前面加上extern “C”
在C++这个工程里面对每一个声明加上extern "C"是为了告诉C++这些函数要用C的方式去编译,此时我到C工程项目里面去编译运行:
发现这里又出错了,这是为什么呢??这是因为我们的头文件展开出错,在这个C的项目前面我们包了头文件Stack.h,包上的这个头文件中里面就加上了我们先前的extern "C",此时出错理所应当,因为C语言不支持extern"C"。
此时我们巧用条件编译:
下面解释一下上图红框框里面的意思:
- 如若满足C++的标准,那么就把EXTERN_C替换成extern"C",让其在C++工程中将这些函数用C语言的标准去访问,如若不满足C++标准,那么就把EXTERN_C看为空,啥也没有,这样在C项目工程那链接的时候,根本不会出现EXTERN_C,又满足了链接要求。
我们在C项目工程里面编译运行看看:
编译运行成功。
- 法二: 为了避免重复写EXTERN_C,我们先在C++项目工程里面把声明的函数用extern"C"整体包起来:
此时我们对C项目进行编译运行:
此时编译运行同样是没有任何错误。