C++
从 C 到 C++
起源
-
早期并没有“C++”这个名字,而是叫做“带类的C”,作为C语言的一个扩展和补充出现,增加了很多新的语法,目的是提高开发效率(与 Servlet 和 JSP 的关系类比)
-
这个时期的 C++ 非常粗糙,仅支持简单的面向对象编程,也没有自己的编译器,而是通过一个预处理程序(cfront),先将 C++ 代码”翻译“为C语言代码,再通过C语言编译器合成最终的程序
-
随着 C++ 的流行,语法越来越强大,已经能够很完善的支持面向过程编程、面向对象编程和泛型编程,几乎成了一门独立的语言,拥有了自己的编译方式
-
我们很难说 C++ 拥有独立的编译器,例如:Windows 下的微软编译器(cl.exe)、Linux 下的 GCC 编译器、Mac 下的 Clang 编译器(已经是 Xcode 默认编译器,雄心勃勃,立志超越 GCC),它们都同时支持C语言和 C++,统称为 C/C++ 编译器。对于C语言代码,它们按照C语言的方式来编译;而对于 C++ 代码,就按照 C++ 的方式编译
OOP
-
在C语言中,把重复使用或具有某项功能的代码封装成一个函数,将拥有相关功能的多个函数放在一个源文件,并提供对应的头文件,这就是一个模块。使用模块时,只要引入对应的头文件
-
而在 C++ 中,多了一层封装,就是类(Class)。类由一组相关联的函数、变量组成,可以将一个类或多个类放在一个源文件,使用时引入对应的类就可以
-
不要小看类(Class)这一层封装,它有很多特性,极大地方便了中大型程序的开发,并让 C++ 成为面向对象的语言
-
面向对象编程在代码执行效率上绝对没有任何优势,它的主要目的是方便组织和管理代码,快速梳理编程思路,带来编程思想上的革新
-
面向对象编程是针对开发中大规模的程序而提出来的,目的是提高软件开发的效率。但不要把面向对象和面向过程对立起来,面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。如果你希望开发一个贪吃蛇游戏,类和对象或许是多余的,几个函数就可以搞定;但如果开发一款大型游戏,你绝对离不开面向对象
编译方式
-
C++源文件后缀
-
C、cc、cxx
-
cpp
-
cpp、cxx、cc、c++、C
-
cpp、cxx、cc
-
Microsoft Visual C++
-
GCC(GNU C++)
-
Borland C++
-
UNIX
-
-
Linux GCC
-
gcc main.cpp module.cpp -lstdc++
-
g++ main.cpp -o demo
-
使用C++库链接
-
指定名称
-
gcc main.c module.c
-
C语言
-
C++
-
GCC 由 GUN 组织,最初只支持C语言,是一个单纯的C语言编译器。后来 GNU 组织倾注了更多精力,GCC 越发强大,增加了对 C++、Objective-C、Fortran、Java 等语言的支持,此时的 GCC 就成了一个编译器套件(套装),是所有编译器的总称
-
gcc命令也做了相应地调整,不再仅仅支持C语言,而是默认支持C语言,增加参数后可以支持其他的语言。即是一个通用命令,根据不同的参数调用不同的编译器或链接器
-
但是让用户指定参数是一种不明智的行为,不但增加了学习成本,还使得操作更加复杂,所以后来 GCC 针对不同的语言推出了不同的命令,例如g++命令用来编译 C++,gcj命令用来编译 Java,gccgo命令用来编译Go语言
-
命名空间与头文件
-
命名空间
-
name 是命名空间的名字,可以包含变量、函数、类、typedef、#define 等,最后由 {} 包围
-
为解决不可避免的变量或函数的命名冲突
-
namespace name { // variables, functions, classes}
-
:: 称为域解析操作符(也称作用域运算符或作用域限定符),指明要使用的命名空间
-
除了直接使用域解析操作符,还可以采用 using 关键字声明。不仅可以针对命名空间中的一个变量,也可以用于声明整个命名空间
-
-
头文件
-
旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中,位于全局作用域,这也是 C++ 标准所规定的
-
新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似但不完全对应,头文件的内容在命名空间 std 中
-
标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中
-
具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,只是内容在 std 中
-
早期的 C++ 还不完善,不支持命名空间,没有自己的编译器,而是将 C++ 代码翻译成C代码。这个时候 C++ 仍然使用C语言的库:stdio.h、stdlib.h、string.h
-
C++ 也开发了一些新的库,增加自己的头文件。C++ 头文件仍然以 .h 为后缀,它们所包含的类、函数、宏等都是全局范围的,例如:iostream.h:控制台输入输出头文件;fstream.h:文件操作头文件;complex.h:复数计算头文件。
-
后来 C++ 引入了命名空间的概念,计划重新编写库,将类、函数、宏等都统一纳入一个命名空间,名为std,即“标准命名空间”
-
但是这时已有很多老式 C++ 开发的程序了,并没有使用命名空间,直接修改原来的库会带来一个很严重的后果:程序员会因为不愿花费大量时间修改老式代码而极力反抗,拒绝使用新标准的 C++ 代码
-
C++ 开发人员想了一个好办法:保留原来的库和头文件,它们在 C++ 中可以继续使用;然后再把原来的库复制一份,在此基础上稍加修改,把类、函数、宏等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用,新开发的程序可以使用新版的 C++ 库
-
为了避免头文件重名,新版 C++ 库对头文件的命名做了调整,去掉了后缀 .h:iostream、fstream 等等;而对于原来C语言的头文件,变成了:cstdio、cstdlib 等等
-
头文件小结
-
需要注意的是,旧的 C++ 头文件是官方所反对使用的,已明确提出不再支持,但旧的C头文件仍然可以使用,以保持对C的兼容性。实际上,编译器开发商不会停止对客户现有软件提供支持,可以预计,旧的 C++ 头文件在未来数年内还是会被支持
-
不过现实情况和 C++ 标准所期望的有些不同。对于原来C语言的头文件,即使按照 C++ 的方式来使用,即 #include <cstdio> 这种形式,那么符号可以位于命名空间 std 中,也可以位于全局范围中。Microsoft Visual C++ 和 GCC下都能够编译通过,也就是说,大部分编译器在实现时并没有严格遵循 C++ 标准
-
标准写法会一直被编译器支持,非标准写法可能会在以后的升级版本中不再支持
-
虽然 C++ 几乎完全兼容C语言,C语言的头文件在 C++ 中依然被支持,但 C++ 新增的库更加强大和灵活,尽量使用这些 C++ 新增的头文件,例如 iostream、fstream、string 等
-
将 std 直接声明在所有函数外部,虽然使用方便,但在中大型项目开发中是不被推荐的,会增加命名冲突的风险,推荐在函数内部声明 std
-
变量位置
-
C89 规定,所有局部变量都必须定义在函数开头,在定义好变量之前不能有其他的执行语句。C99 标准取消了这这条限制。但是 VC/VS 对 C99 的支持很不积极,仍然要求变量定义在函数开头
-
.c:可以在 GCC、Xcode 下编译通过,但在 VC/VS 下会报错。GCC、Xcode 对 C99 的支持非常好,可以在函数的任意位置定义变量;但 VC/VS 对 C99 的支持寥寥无几,必须在函数开头定义好所有变量
-
.cpp:在 GCC、Xcode、VC/VS 下都可以编译通过。这是因为 C++ 取消了原来的限制,变量只要在使用之前定义好即可,不强制必须在函数开头定义所有变量
-
取消限制带来的一个好处是,可以在 for 循环的控制语句中定义变量,让代码看起来更加紧凑,使得的作用域被限制在 for 循环语句内部,减小了命名冲突的概率
const
-
内存中的 const
-
C++ 中的 const 变量虽然也会占用内存,也能使用 & 获取得它的地址
-
const int m = 10;int n = m;
-
在C语言中,编译器先到 m 所在的内存中取出数据,再赋给 n;而在 C++ 中,编译器直接将 10 赋给 m,没有读取内存的过程,和 int n = 10; 的效果一样。C++ 中的常量更类似于 #define 命令,是一个值替换的过程,只不过 #define 是在预处理阶段替换,而常量在编译阶段替换,并且会进行类型检查
-
C++ 对 const 的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值
-
-
const 作用域
-
C++ 对 const 的特性做了调整,全局 const 变量的作用域仍然是当前文件,在其他文件中是不可见的,和添加了 static 关键字的效果类似
-
由于 C++ 中全局 const 变量的可见范围仅限于当前源文件,所以可以放在头文件中,这样可以被包含多次
-
C和 C++ 中全局 const 变量的作用域相同,都是当前文件,不同的是它们的可见范围:C语言中 const 全局变量的可见范围是整个程序,在其他文件中使用 extern 声明后就可以使用;而 C++ 中 const 全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以可以定义在头文件中,可多次引入
-
如果使用的是 GCC,可以通过添加 extern 关键字来增大 C++ 全局 const 变量的可见范围
-
内联函数(内嵌函数、内置函数)
-
减少函数调用开销
-
为了消除函数调用的时空开销,在编译时将函数调用处用函数体替换。类似于宏展开
-
inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。在函数声明处添加 inline 关键字是无效的,被编译器会忽略
-
更为严格地说,内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格:由于内联函数比较短小,通常省略函数原型,将整个函数定义放在本应该提供函数原型的地方
-
使用内联函数的缺点也是非常明显的,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数
-
对函数作 inline 声明只是程序员对编译器提出的一个建议,不是强制性的,并非一经指定为 inline 编译器就必须这样做。编译器有自己的判断能力,根据具体情况决定是否这样做
-
-
内联函数与宏
-
在编写 C++ 代码时推荐使用内联函数替换带参数的宏
-
和宏一样,内联函数可以定义在头文件中(不用加 static 关键字),并且头文件被多次 #include 后也不会引发重复定义错误。这一点和非内联函数不同,非内联函数是禁止定义在头文件中的,它所在的头文件被多次 #include 后会引发重复定义错误
-
而将内联函数的声明和定义分散到不同的源文件,链接时会出错。编译期间用内联函数替换函数调用处,编译完成后函数就不存在了,链接器在将多个目标文件(.o 或 .obj 文件)合并成一个可执行文件时找不到该函数的定义
-
内联函数虽然叫做函数,在定义和声明的语法上也和普通函数一样,但它已经失去了函数的本质。函数是一段可以重复使用的代码,位于虚拟地址空间中的代码区,也占用可执行文件的体积;而内联函数的代码在编译后就被消除了,不存在于虚拟地址空间中,不重复使用
-
将内联函数作为带参宏的替代方案更为靠谱,而不是真的当做函数使用
-
内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误。这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏
-
默认参数
-
void func(int a, char c = '@', float b = 1 + 2.9) {}
-
指定了默认参数后,调用时可以省略该实参
-
C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,其后的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提
-
通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量
-
除了函数定义,你也可以在函数声明处指定默认参数。不过当出现函数声明时情况会变得稍微复杂
-
C++ 规定,在给定的作用域中只能指定一次默认参数。若定义和声明位于同一个源文件,它们的作用域也就都是整个源文件,这样就导致在同一个文件作用域中指定了两次默认参数,违反了 C++ 的规定
-
C语言有四种作用域,分别是函数原型作用域、局部作用域(函数作用域)、块作用域、文件作用域(全局作用域),C++ 也有这几种作用域
-
编译器使用的是当前作用域中的默认参数。站在编译器的角度看,它不管当前作用域中是函数声明还是函数定义,只要有默认参数就可以使用
-
在多文件编程时,我们通常的做法是将函数声明放在头文件中,并且一个函数只声明一次,但是多次声明同一函数也是合法的
-
不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认参数。函数的后续声明只能为之前那些没有默认值的形参添加默认值,而其右侧的所有形参必须都有默认值
-
函数重载
-
C++ 允许多个函数拥有相同的名字,只要参数列表不同就可以
-
参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序
-
重载函数的返回类型可以相同也可以不相同
-
C++ 代码在编译时会根据参数列表对函数进行重命名。例如 void Swap(int a, int b) 会被重命名为 _Swap_int_int(不同的编译器有不同的重命名方式)。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错。这叫做重载决议(Overload Resolution)
-
从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样
-
重载决议的优先级不同于四则运算
-
整型转换
-
小数转换
-
整数和小数转换
-
指针转换
-
char 到 long、short 到 long、int 到 short、long 到 char
-
double 到 float
-
int 到 double、short 到 float、float 到 int、double 到 long
<
-

本文详细介绍了C++中的继承、多态以及虚函数的概念和用法。讲解了继承的三种方式(public、private、protected),多态的实现机制,以及虚函数在实现多态中的关键作用。同时探讨了虚函数表、虚析构函数、RTTI(运行时类型识别)和动态类型。通过对这些内容的深入理解,有助于提升C++的面向对象编程能力。
最低0.47元/天 解锁文章
819

被折叠的 条评论
为什么被折叠?



