第1章 从C到C++总结
C++支持:
- 面向过程编程、
- 面向对象编程(OOP),
- 泛型编程。
我们很难说C++拥有独立的编译器,例如,
- Windows 下的微软编译器(cl.exe)、
- Linux 下的 GCC 编译器、
- Mac 下的 Clang 编译器(已经是 Xcode 默认编译器,雄心勃勃,立志超越 GCC),
它们都同时支持C语言和C++,统称为 C/C++ 编译器。
- 对于C语言代码,它们按照C语言的方式来编译;
- 对于C++代码,就按照C++的方式编译。
从表面上看,C、C++ 代码使用同一个编译器来编译,所以上面我们说“后期的 C++ 拥有了自己的编译方式”,而没有说“C++ 拥有了独立的编译器”。
学习编程是一个循序渐进的过程,不要期望一口吃个胖子,不要贪多嚼不烂。
每个初学者都经历过这样的窘境:
- 已经学习了语法,明白了编程语言都有什么,
- 也按照教程敲了不少代码,
- 但是遇到实际问题就挂了,没有思路,不知道从何下手。
说白了就是只会学不会用。究其原因,就是实践少,没有培养起编程思维!学习知识容易,运用知识难!
学习法宝:自己多动手写代码并观察运行结果。
类是一个通用的概念,C++、Java、C#、PHP 等很多编程语言中都支持类,都可以通过类创建对象。可以将类看做是结构体的升级版,C语言的晚辈们看到了C语言的不足,尝试加以改善,继承了结构体的思想,并进行了升级,让程序员在开发或扩展大中型项目时更加容易。
- 在C语言中,我们会把重复使用或具有某项功能的代码封装成一个函数,将拥有相关功能的多个函数放在一个源文件,再提供一个对应的头文件,这就是一个模块。使用模块时,引入对应的头文件就可以。
- 而在C++中,多了一层封装,就是类(Class)。类由一组相关联的函数、变量组成,你可以将一个类或多个类放在一个源文件,使用时引入对应的类就可以。
下面是C和C++项目组织方式的对比:
不要小看类(Class)这一层封装,它有很多特性,极大地方便了中大型程序的开发,它让C++成为面向对象的语言。
面向对象编程在代码执行效率上绝对没有任何优势,它的主要目的是:
- 方便程序员组织和管理代码,
- 快速梳理编程思路,
- 带来编程思想上的革新。
面向对象编程是针对开发中大规模的程序而提出来的,目的是提高软件开发的效率。不要把面向对象和面向过程对立起来,面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。如果你希望开发一个贪吃蛇游戏,类和对象或许是多余的,几个函数就可以搞定;但如果开发一款大型游戏,那你绝对离不开面向对象。
C++ 和C语言类似,也要经过编译和链接后才能运行。VC 6.0、VS、GCC、Xcode 等常见 IDE 或编译器,它们除了可以运行C语言程序,还可以运行 C++ 程序,步骤是类似的,读者需要留意的是 C++ 源文件的后缀,以及 GCC 中的g++命令。
下图是 C/C++ 代码生成可执行文件的过程:
C语言源文件的后缀非常统一,在不同的编译器下都是.c。C++源文件的后缀则有些混乱,不同的编译器支持不同的后缀,下表是一个简单的汇总:
我推荐使用.cpp作为 C++ 源文件的后缀,这样更加通用和规范。
- 在C语言中,我们使用gcc命令来编译和链接C程序。
- 例如编译单个源文件:
gcc main.c
-
- 编译多个源文件:
gcc main.c module.c
- 编译C++程序时,gcc命令也可以使用,不过要增加-lstdc++选项,否则会发生链接错误。gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。(个人:这里的l应该是link的意思吧)
- 例如编译单个源文件:
gcc main.cpp -lstdc++
-
- 编译多个源文件:
gcc main.cpp module.cpp -lstdc++
- 不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法如出一辙,
- 例如编译单个源文件:
g++ main.cpp
-
- 编译多个源文件:
g++ main.cpp module.cpp
-
- 使用-o选项可以指定可执行文件的名称:(个人:可以看到-o选项的位置可以是任意的)
g++ main.cpp -o demo ./demo
要想理解g++命令,我们得从 GCC 的历史谈起。GCC 是由 GUN 组织开发的,最初只支持C语言,是一个单纯的C语言编译器,后来 GNU 组织倾注了更多的精力,使得 GCC 越发强大,增加了对 C++、Objective-C、Fortran、Java 等其他语言的支持,此时的 GCC 就成了一个编译器套件(套装),是所有编译器的总称。在这个过程中,gcc命令也做了相应地调整,它不再仅仅支持C语言,而是默认支持C语言,增加参数后也可以支持其他的语言。也就是说,gcc是一个通用命令,它会根据不同的参数调用不同的编译器或链接器。
但是让用户指定参数是一种不明智的行为,不但增加了学习成本,还使得操作更加复杂,所以后来 GCC 又针对不同的语言推出了不同的命令,例如
- g++命令用来编译 C++,
- gcj命令用来编译 Java,
- gccgo命令用来编译Go语言。
在以后使用 Linux GCC 时,我推荐使用g++命令来编译 C++ 程序,这样更加简洁和规范。
在C语言中,我们通常会使用 scanf 和 printf 来对数据进行输入输出操作。在C++语言中,C语言的这一套输入输出库我们仍然能使用,但是 C++ 又增加了一套新的、更容易使用的输入输出库。
【例1】简单的输入输出代码示例:
#include<iostream> using namespace std; int main(){ int x; float y; cout<<"Please input an int number:"<<endl; cin>>x; cout<<"The int number is x= "<<x<<endl; cout<<"Please input a float number:"<<endl; cin>>y; cout<<"The float number is y= "<<y<<endl; return 0; }
运行结果如下(↙表示按下enter键):
Please input an int number: 8↙ The int number is x= 8 Please input a float number: 7.4↙ The float number is y= 7.4
C++ 中的输入与输出可以看做是一连串的数据流,
- 输入即可视为从文件或键盘中输入程序中的一串数据流,
- 而输出则可以视为从程序中输出一连串的数据流到显示屏或文件中。
在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件iostream,它包含了用于输入输出的对象,例如常见的cin表示标准输入、cout表示标准输出、cerr表示标准错误。iostream 是 Input Output Stream 的缩写,意思是“输入输出流”。cout 和 cin 都是 C++ 的内置对象,而不是关键字。C++ 库定义了大量的类(Class),程序员可以使用它们来创建对象,cout 和 cin 就分别是 ostream 和 istream 类的对象,只不过它们是由标准库的开发者提前创建好的,可以直接拿来使用。这种在 C++ 中提前创建好的对象称为内置对象。
使用 cout 进行输出时需要紧跟<<运算符,使用 cin 进行输入时需要紧跟>>运算符,这两个运算符可以自行分析所处理的数据类型,因此无需像使用 scanf 和 printf 那样给出格式控制字符串。
- endl表示换行,它是“end of line”的缩写,与C语言里的/n作用相同(个人:还有刷新缓冲区)。当然这段代码中也可以用\n来替代endl,这样就得写作:
cout<<"Please input an int number:\n";
- 第7行代码表示从标准输入(键盘)中读入一个 int 型的数据并存入到变量 x 中。如果此时用户输入的不是 int 型数据,则会被强制转化为 int 型数据。 (个人:但是遇到将一个字符类型如a解析为int时,会解析读取失败,这和我们int i = 'a',此时i=97时的情况还不一样)
- 第8行代码将输入的整型数据输出。从该语句中我们可以看出 cout 能够连续地输出。
同样 cin 也是支持对多个变量连续输入的,如下所示。
【例2】cin 连续输入示例:
#include<iostream> using namespace std; int main(){ int x; float y; cout<<"Please input an int number and a float number:"<<endl; cin>>x>>y; cout<<"The int number is x= "<<x<<endl; cout<<"The float number is y= "<<y<<endl; return 0; }
运行结果:
Please input an int number and a float number: 8 7.4↙ The int number is x= 8 The float number is y= 7.4
第7行代码连续从标准输入中读取一个整型和一个浮点型数字(默认以空格分隔),分别存入到 x 和 y 中。
输入运算符>>在读入下一个输入项前会忽略前一项后面的空格,所以数字 8 和 7.4 之间要有一个空格,当 cin 读入 8 后忽略空格,接着读取 7.4。
cout、cin 的用法非常强大灵活,本节所展示的只是最基本的功能,更多高级技巧将在后续章节中介绍。在以后的 C++ 编程中,我也推荐大家使用 cin、cout,它们比C语言中的 scanf、printf 更加灵活易用。
C语言并没有彻底从语法上支持“真”和“假”,只是用 0 和非 0 来代表。这点在 C++ 中得到了改善,C++ 新增了 bool 类型(布尔类型),它一般占用 1 个字节长度。bool 类型只有两个取值,true 和 false:true 表示“真”,false 表示“假”。bool 是类型名字,也是 C++ 中的关键字,它的用法和 int、char、long 是一样的,
#include <iostream> using namespace std; int main(){ int a, b; bool flag; //定义布尔变量 cin>>a>>b; flag = a > b; cout<<"flag = "<<flag<<endl; return 0; }
输出结果:
10 20↙ flag = 0
遗憾的是,在 C++ 中使用 cout 输出 bool 变量的值时还是用数字 1 和 0 表示,而不是 true 或 false。Java、PHP、JavaScript 等也都支持布尔类型,但输出结果为 true 或 false,我武断地认为这样更科学。
在以后的编码中,我推荐使用 bool 变量来表示逻辑运算、关系运算以及开关变量的值。
在C语言中,const 用来限制一个变量,表示这个变量不能被修改,我们通常称这样的变量为常量(Constant),在C++中,const 的含义并没有改变,只是对细节进行了一些调整,以下是最主要的两点。
C++中的 const 更像编译阶段的 #define:
先来看下面的两条语句:
const int m = 10; int n = m;
我们知道,变量是要占用内存的,即使被 const 修饰也不例外。m、n 两个变量占用不同的内存,int n = m;表示将 m 的值赋给 n,这个赋值的过程在C和C++中是有区别的。
- 在C语言中,编译器会先到 m 所在的内存取出一份数据,再将这份数据赋给 n;
- 而在C++中,编译器会直接将 10 赋给 m,没有读取内存的过程,和int n = 10;的效果一样。
C++ 中的常量更类似于#define命令,是一个值替换的过程,只不过#define是在预处理阶段替换,而常量是在编译阶段替换。 C++ 对 const 的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值。
有读者提出疑问,const 变量不是禁止被修改吗?对,这种说法没错!不过这只是语法层面上的限制,通过指针仍然可以修改。下面的代码演示了如何通过指针修改 const 变量:
#include <stdio.h> int main(){ const int n = 10; int *p = (int*)&n; //必须强制类型转换 *p = 99; //修改const变量的值 printf("%d\n", n); return 0; }
注意,&n得到的指针的类型是const int *,必须强制转换为int *后才能赋给 p,否则类型是不兼容的。
- 将代码放到.c文件中,以C语言的方式编译,运行结果为99。
- 再将代码放到.cpp文件中,以C++的方式编译,运行结果就变成了10。
这种差异正是由于C和C++对 const 的处理方式不同造成的。
- 在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。
- 而在C++中,printf("%d\n", n);语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);一样,不管 n 所在的内存如何变化,都不会影响输出结果。
当然,这种修改常量的变态代码在实际开发中基本不会出现,本例只是为了说明C和C++对 const 的处理方式的差异:
- C语言对 const 的处理和普通变量一样,会到内存中读取数据;
- C++ 对 const 的处理更像是编译时期的#define,是一个值替换的过程。
C++中全局 const 变量的可见范围是当前文件:
我们知道,普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern声明后就可以使用,下面是多文件编程的演示代码:
代码段1(源文件1):
#include <stdio.h> int n = 10; void func(); int main(){ func(); printf("main: %d\n", n); return 0; }
代码段2(源文件2):
#include <stdio.h> extern int n; void func(){ printf("module: %d\n", n); }
不管是以C还是C++的方式编译,运行结果都是:
在C语言中,const 变量和普通变量一样,在其他源文件中也是可见的。修改代码段1,在 n 的定义前面加 const 限制,如下所示:
const int n = 10;
修改后的代码仍然能够正确编译,运行结果和上面也是一样的。这说明C语言中的 const 变量在多文件编程时的表现和普通变量一样,除了不能修改,没有其他区别。
module.h 代码:
const int n = 10; void func();
module.cpp 代码:
#include <stdio.h> #include "module.h" void func(){ printf("module: %d\n", n); }
main.cpp 代码:
#include <stdio.h> #include "module.h" int main(){ func(); printf("main: %d\n", n); return 0; }
C和C++中全局 const 变量的作用域相同,都是当前文件,不同的是它们的可见范围:
- C语言中 const 全局变量的可见范围是整个程序,在其他文件中使用 extern 声明后就可以使用;
- 而C++中 const 全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以它可以定义在头文件中,多次引入后也不会出错。
总结:
- C++ 中的 const 变量,虽然也会占用内存,也能使用&获取得它的地址,但是在使用时却更像编译时期的#define;可见范围也仅限于当前文件。
- 很多C++教程在对比 const 和 #define 的优缺点时提到,#define 定义的常量仅仅是字符串的替换,不会进行类型检查,而 const 定义的常量是有类型的,编译器会进行类型检查,相对来说比 #define 更安全,所以鼓励大家使用 const 代替 #define。
在C语言中,动态分配内存用 malloc() 函数,释放内存用 free() 函数。如下所示:
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间 free(p); //释放内存
C++中,这两个函数仍然可以使用,但是C++又新增了两个关键字,new 和 delete:new 用来动态分配内存,delete 用来释放内存。
用 new 和 delete 分配内存更加简单:
int *p = new int; //分配1个int型的内存空间 delete p; //释放内存
new 操作符会根据后面的数据类型来推断所需空间的大小。如果希望分配一组连续的数据,可以使用 new[]:
int *p = new int[10]; //分配10个int型的内存空间 delete[] p;
通常 new 和 delete、new[] 和 delete[] 操作符应该成对出现,并且不要和C语言中 malloc()、free() 一起混用。
在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,最明显的是可以自动调用构造函数和析构函数。 (个人:new和delete是C++中的操作符,而不是像malloc和free是函数,需要包含stdlib.h头文件,new和delete因为是操作符,所以可以在代码中直接使用,不用包含任何头文件)
C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。
- 下面的写法是正确的:
void func(int a, int b=10, int c=20){ } void func(int a, int b, int c=20){ }
- 但这样写不可以:
void func(int a, int b=10, int c=20, int d){ } void func(int a, int b=10, int c, int d=20){ }
默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在以后设计类时你将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
除了函数定义,你也可以在函数声明处指定默认参数。不过当出现函数声明时情况会变得稍微复杂,有时候你可以在声明处和定义处同时指定默认参数,有时候你只能在声明处指定,请看下面的例子。
- 示例1:
#include <iostream> using namespace std; void func(int a, int b = 10, int c = 36); int main(){ func(99); return 0; } void func(int a, int b = 10, int c = 36){ cout<<a<<", "<<b<<", "<<c<<endl; }
这段代码在编译时会报错,
- 对代码稍作修改,将 func() 函数的定义放到其他源文件中,如下所示。示例2:
main.cpp 代码:
#include <iostream> using namespace std; void func(int a, int b = 10, int c = 16); int main(){ func(99); return 0; }
module.cpp 代码:
#include <iostream> using namespace std; void func(int a, int b = 10, int c = 36){ cout<<a<<", "<<b<<", "<<c<<endl; }
运行结果:
C++ 规定,在给定的作用域中只能指定一次默认参数。
- 对于示例1,func() 的定义和声明位于同一个源文件,这样就导致在同一个文件作用域中指定了两次默认参数,违反了 C++ 的规定。
- 对于示例2,func() 的声明位于main.cpp,作用域也是main.cpp,而 func() 的定义位于module.cpp,作用域也是module.cpp,func() 的声明和定义位于不同的作用域,相互之间不影响。
继续对代码进行修改,将 func() 定义处 b、c 的默认值分别设置为 5、57,而声明处 b、c 的默认值不变,依然为 10、16。编译并运行程序,发现输出结果与上面一样,这说明编译器使用的是当前作用域中的默认参数。站在编译器的角度看,它不管当前作用域中是函数声明还是函数定义,只要有默认参数就可以使用。
在多文件编程时,我们通常的做法是将函数声明放在头文件中,并且一个函数只声明一次,但是多次声明同一函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认参数。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认值,而且该形参右侧的所有形参必须都有默认值。为了说明问题,我们不妨对 main.cpp 中的代码稍作修改:
#include <iostream> using namespace std; //多次声明同一个函数 void func(int a, int b, int c = 36); void func(int a, int b = 5, int c); int main(){ func(99); return 0; }
输出结果:
这种声明方式是正确的。第一次声明时为 c 指定了默认值,第二次声明时为 b 指定了默认值;第二次声明是添加默认参数。需要提醒的是,第二次声明时不能再次给 c 指定默认参数,否则就是重复声明同一个默认参数。
C++ 标准库已经提供了交换两个变量的值的函数,它的名字就是swap,位于algorithm头文件中。
重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。
- 重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活。
- 在使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人觉得莫名其妙。
- 注意,参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值不能作为重载的依据。
函数的重载的规则:
- 函数名称必须相同。
- 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载。
C++是如何做到函数重载的?:C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_int,void Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
发生函数调用时编译器会根据传入的实参的个数、类型、顺序等信息去匹配要调用的函数,这在大部分情况下都能够精确匹配。但当实参的类型和形参的类型不一致时情况就会变得稍微复杂。
#include <iostream> using namespace std; //1号函数 void func(char ch){ cout<<"#1"<<endl; } //3号函数 void func(long m){ cout<<"#3"<<endl; } //4号函数 void func(double f){ cout<<"#4"<<endl; } int main(){ short s = 99; float f = 84.6; func('a'); func(s); func(49); func(f); return 0;
这段代码在编译时发生了错误,
这着实有点让人摸不着头脑!根据以往的编程经验,s 和 49 不都应该被转换成 long 类型,从而匹配3号函数void func(long m)吗?这种推论在一般的函数调用或者四则运算中确实没错,但它不一定适用于重载函数!
C++ 标准规定,在进行重载决议时编译器应该按照下面的优先级顺序来处理实参的类型:
优先级 | 包含的内容 | 举例说明 |
---|---|---|
精确匹配 | 不做类型转换,直接匹配 | (暂无说明) |
只是做微不足道的转换 |
|
|
类型提升后匹配(个人:同种类型的提升) | 整型提升 |
|
小数提升 | 从 float 提升为 double。 | |
使用自动类型转换后匹配(个人:这里不用管这里的举例,只要能在赋值表达式中,自动转换成功就行) | 整型转换 | 从 char 到 long、short 到 long、int 到 short、long 到 char。 |
小数转换 | 从 double 到 float。 | |
整数和小数转换 | 从 int 到 double、short 到 float、float 到 int、double 到 long。 | |
指针转换 | 从 int * 到 void *。 |
C++ 标准还规定,编译器应该按照从高到低的顺序来搜索重载函数,
- 首先是精确匹配,
- 然后是类型提升,
- 最后才是类型转换;
一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。如果在一个优先级中找到多个(两个以及以上)合适的重载函数,编译器就会陷入两难境地,不知道如何抉择,编译器会将这种模棱两可的函数调用视为一种错误,因为这些合适的重载函数同等“优秀”,没有一个脱颖而出,调用谁都一样。这就是函数重载过程中的二义性错误。func(s)、func(49)没有精确匹配的重载函数,将它们的类型都提升为 int 后仍然不能匹配,接下来进入自动类型转换阶段,发现 s(类型为short) 被转换为 char(整型转换)、long(整型转换)、double(整数和小数转换)后都有比较合适的函数,而且它们在同一个优先级中,谁也不比谁优秀,调用哪个都一样,产生了二义性,所以编译器会报错。
注意,类型提升和类型转换不是一码事!
- 类型提升是积极的,是为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低;
- 而类型转换是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。
- 类型提升只有上表中列出的几种情况,其他情况都是类型转换。
#include <iostream> using namespace std; void func(long ch){ cout<<"long"<<endl; } void func(char ch){ cout<<"char"<<endl; } int main(){ int n = 1000; func(n); return 0; }
发生编译错误:
当重载函数有多个参数时也会产生二义性,而且情况更加复杂。
C++ 标准规定,如果有且只有一个函数满足下列条件,则匹配成功:
- 该函数对每个实参的匹配都不劣于其他函数;(个人:至少持平)
- 至少有一个实参的匹配优于其他函数。
假设现在有以下几个函数原型:
void func(int, int); //① void func(char, int, float); //② void func(char, long, double); //③
我们来分析如下的调用会发生什么情况:
short n = 99; func('@', n, 99); func('@', n, 99.5);
函数原型func(int, int)只有两个参数,而函数调用有三个参数,很容易看出来不匹配,在初次筛选时就会被过滤掉,接下来我们只讨论②③个函数原型。
- 先来看第一个函数调用。如果只考虑第一个实参'@',那么②③两个函数都能够精确匹配,谁也不比谁优秀,是平等的;如果只考虑第二个实参n,对于②,需要把 short 提升为 int(类型提升),对于③,需要把 short 转换为 long(类型转换),类型提升的优先级高于类型转换,所以②胜出;如果只考虑第三个实参99,②③都要进行类型转换,没有哪一个能胜出,它们是平等的。从整体上看,②③在第一、三个实参的匹配中是平等的,但②在第二个实参的匹配中胜出,也就是说,②对每个实参的匹配都不劣于③,但有一个实参的匹配优于③,所以②最终脱颖而出,成为被调用函数。
- 再来看第二个函数调用。只考虑第一个实参时②③是平等的,没有谁胜出;只考虑第二个实参时②胜出;只考虑第三个实参时,②需要类型转换,③能够精确匹配,精确匹配的优先级高于类型转换,所以③胜出。从整体上看,②③在第一个实参的匹配中是平等的,②在第二个实参的匹配中胜出,③在第三个实参的匹配中胜出,它们最终“打成了平手”,分不清孰优孰劣,所以编译器不知道如何抉择,会产生二义性错误。
例如,我们要设计几个重载函数来处理数值,数值包括小数和整数,如果只提供了以下两个函数原型:
void func(int); void func(double);
那么下面的函数调用就会产生二义性错误:
long n = 1000; func(n);
n 是 long 类型,转换为 int 或 double 的优先级都是一样的,编译器不知道如何抉择。
在一个项目中,能否既包含 C++ 程序又包含C程序呢?换句话说,C++ 和 C 可以进行混合编程吗?
要知道,在 C++ 出现之前,很多实用的功能都是用 C 语言开发的,很多底层的库也是用 C 语言编写的。这意味着,如果能在C++代码中兼容C语言代码,无疑能极大地提高 C++ 程序员的开发效率。
而恰恰答案也正是我们想要的,C++ 和 C 可以进行混合编程。但需要注意的是,由于 C++ 和 C 在程序的编译、链接等方面都存在一定的差异,而这些差异往往会导致程序运行失败。
举个例子,如下就是一个用 C++ 和 C 混合编程实现的实例项目:
//myfun.h void display(); //myfun.c #include <stdio.h> #include "myfun.h" void display(){ printf("hello world!"); } //main.cpp #include <iostream> #include "myfun.h" using namespace std; int main(){ display(); return 0; }
在此项目中,主程序是用 C++ 编写的,而 display() 函数的定义是用 C 语言编写的。从表面上看,这个项目很完整,我们可以尝试运行它:
In function `main': undefined reference to `display()'
如上是调用 GCC 编译器运行此项目时给出的错误信息,指的是编译器无法找到 main.cpp 文件中 display() 函数的实现代码。导致此错误的原因,就是因为 C++ 和 C 编译程序的方式存在差异。
(个人:如果使用g++命令编译运行,能够成功,g++ -o main main.cpp myfunc.c)
之所以 C++ 支持函数的重载,是因为 C++ 会在程序的编译阶段对函数的函数名进行“再次重命名”,例如:
- void Swap(int a, int b) 会被重命名为_Swap_int_int;
- void Swap(float x, float y) 会被重命名为_Swap_float_float。
显然通过重命名,可以有效避免编译器在程序链接阶段无法找到对应的函数。
但是,C 语言是不支持函数重载的,它不会在编译阶段对函数的名称做较大的改动。仍以 void Swap(int a, int b) 和 void Swap(float x, float y) 为例,若以 C 语言的标准对它们进行编译,两个函数的函数名将都是_Swap。
不同的编译器有不同的重命名方式,
- 但根据 C++ 标准编译后的函数名几乎都由原有函数名和各个参数的数据类型构成,
- 而根据 C 语言标准编译后的函数名则仅有原函数名构成。
这里仅仅举例说明,实际情况可能并非如此。
这也就意味着,使用C和C++进行混合编程时,考虑到对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。
幸运的是,C++ 给出了相应的解决方案,即借助 extern "C",就可以轻松解决 C++ 和 C 在处理代码方式上的差异性。extern 是 C 和 C++ 的一个关键字,但对于 extern "C",读者大可以将其看做一个整体,和 extern 毫无关系。extern "C" 既可以修饰一句 C++ 代码,也可以修饰一段 C++ 代码,它的功能是让编译器以处理 C 语言代码的方式来处理修饰的 C++ 代码。
仍以本节前面的实例项目来说,
- main.cpp 和 myfun.c 文件中都包含 myfun.h 头文件,当程序进行预处理操作时,myfun.h 头文件中的内容会被分别复制到这 2 个源文件中。
- 对于 main.cpp 文件中包含的 display() 函数来说,编译器会以 C++ 代码的编译方式来处理它;
- 而对于 myfun.c 文件中的 display() 函数来说,编译器会以 C 语言代码的编译方式来处理它。
为了避免 display() 函数以不同的编译方式处理,我们应该使其在 main.cpp 文件中仍以 C 语言代码的方式处理,这样就可以解决函数名不一致的问题。因此,可以像如下这样来修改 myfun.h:
#ifdef __cplusplus extern "C" void display(); #else void display(); #endif
可以看到,当 myfun.h 被引入到 C++ 程序中时,会选择带有 extern "C" 修饰的 display() 函数;反之如果 myfun.h 被引入到 C 语言程序中,则会选择不带 extern "C" 修饰的 display() 函数。由此,无论 display() 函数位于 C++ 程序还是 C 语言程序,都保证了 display() 函数可以按照 C 语言的标准来处理。
(个人:cpp文件默认定义了宏__cplusplus)
在实际开发中,对于解决 C++ 和 C 混合编程的问题,通常在头文件中使用如下格式:
#ifdef __cplusplus extern "C" { #endif void display(); #ifdef __cplusplus } #endif
由此可以看出,extern "C" 大致有 2 种用法,
- 当仅修饰一句 C++ 代码时,直接将其添加到该函数代码的开头即可;
- 如果用于修饰一段 C++ 代码,只需为 extern "C" 添加一对大括号{},并将要修饰的代码囊括到括号内即可。