C++总结

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

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

    • <
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

像向日葵一样~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值