C++一些深入理解
1 虚函数
-
虚继承:在菱形继承中,创立多个基类。
- 还有的编译器是在对象中拥有多个vptr,分别指向不同的虚函数表,这样使用时就不会出错。
- 因为虚继承比较复杂,而且是间接寻址,效率比较低,所以尽量少用。
-
虚函数:
- 每一个拥有虚函数的类都会产生一个指向虚函数表的指针。一个单一继承类中只会有一个虚函数表,每个表会包含全部虚函数的地址。
- 每个虚函数都会被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数关联。比如:索引 0 为类类型信息,索引 1 为虚析构,索引 2 为纯虚函数…等。
- 虚函数实现多态原理:当派生类继承基类时,在基类中利用virtual关键字定义若干个虚函数,会生成基类的虚表A,表中存放着所有虚函数的地址
- 而在派生类中重写某个虚函数func1时,派生类的的虚表B中,会用重写的虚函数地址覆盖原来的函数func1的地址,其它虚函数地址不变。
- 程序采用迟绑定的编译模式,当基类指针指向基类对象时,基类对象的vptr指向虚表A,就会运行基类的func1;而基类指针指向派生类对象时,派生类对象的vptr指向虚表B,就会运行派生类的func1。这样,根据基类指针指向不同的对象,进而调用不同的函数体,就会有不同的表现形式,即实现多态。
- 静态函数、友元函数、构造函数、内联函数均不能声明为虚函数
3 关键字const、static、extern、volatile总结
3.1 关键字const
-
常量修饰
const int a = 10; int const a = 10;
-
指针修饰
int a = 10; //指针指向的内容不能改变 const int* p = &a; int const* p = &a; //指针本身不能改变(指针不能指向其他内容) int* const p = &a; //指针本身,指针指向的内容都不能改变 const int* const p = &a; int const* const p = &a;
-
引用修饰
int a = 100; //不能通过引用修改a const int &a1 = a; int const &a2 = a;
-
函数参数修饰
//传递进来的参数无法改变 void func (const int &n) { n = 10; // 编译错误 }
-
函数返回值修饰
//若函数的返回值是指针,且用const修饰,则函数返回值指向的内容是常数,不可被修改。且此返回值仅能赋值给const修饰的相同类型的指针。 //如果返回值是引用,不能通过引用修改原值 //返回的指针所指向的内容不能修改 const int* func() { //... return p; } //返回的引用无法改变 const int &func() { //... return p; }
-
类成员函数修饰
//不可以修改该类的成员变量 class A{ void func() const; };
-
修饰类对象
//类对象只能调用该对象的const成员函数。 class A { void func() const; }; const A a; a.func();
3.2 关键字static
-
修改变量的生命周期为全局:当static 作用于代码块内部的变量声明时,。从自动变量变为静态变量,但变量的属性和作用域不受影响。
void fun() { static int i = 0; i++; }
-
修改函数或变量的链接属性为文件内:当 static 作用于函数定义时或者用于代码块之外的变量声明时,修改的是其作用域。从外部链接属性(external)变为内部链接属性(internal),但标识符的存储类型和作用域不受影响。也就是说变量或者函数只能在当前源文件中访问,不能在其他源文件中访问。
// a.h static int a = 10; static void fun(); //b.cpp int main() { a = 100; //错误 fun(); //错误 return 0; }
-
修饰类成员变量为类域中的全局变量:对于类的每个对象来说,它是共有的。它在整个程序中只有一份拷贝,只在定义时分配一次内存,供该类所有的对象使用,其值可以通过每个对象来更新。
class A { public: static int num; //声明 }; int A::num = 10; //定义 int main() { A obj; std::cout << obj.num << std::endl; //访问 std::cout << A::num << std::endl; //访问 return 0; }
-
修饰类成员函数分离与类的关系:静态成员函数也是属于类,而不属于任何一个类的实体对象,因此,静态成员函数不含有this指针。同时,它也不能访问类中其它的非静态数据成员和函数。(非静态成员函数可以访问静态数据数据成员和静态成员函数)
class B { public: static void fun(){} }; int main() { B obj; obj.fun(); B::fun(); return 0; }
3.3 关键字extern
-
extern关键字修饰的变量都是全局变量,表示该变量在别的文件中已有声明。
-
若一个变量需要在同一个工程中不同文件里直接使用或修改,则需要将变量做extern声明。只需将该变量在其中一个文件中定义,然后在另外一个文件中使用extern声明便可使用,但两个变量类型需一致。
//1.c int i; //2.c或3.c extern int i; i = 100; //实际使用的是1.c中的
//1.h中声明 extern int i; //1.c文件中定义 int i =0; //其他要使用i变量的c源文件只需要include"1.h"就可以
-
如果在函数首部的最左端冠以关键字extern,则表示此函数是外部函数,可以供其他文件调用。C语言规定,如果在定义函数时省略extern,则隐含为外部函数。在文件中要调用其他文件中的外部函数,则需要在文件中用extern声明该外部函数,然后就可以使用。
3.4 关键字volatile
-
一个使用volatile关键字定义变量,其实就是告诉编译系统这变量可能会被意想不到地改变。那么编译时,编译器就不会自作主张的去假设这个变量的值,而进行代码的优化了。确切的说就是,编译器在编译代码时,优化器每次遇到这个变量,都会到内存中重新读取,而不会使用保存在寄存器里的备份来对代码进行优化。
-
使用的情况
- 在中断服务程序中修改的,供其它程序检测的变量,通常需要定义为volatile;
- 在多任务环境下,各任务间共享的标志,通常也需要定义为volatile;
- 存储器映射的硬件寄存器通常也需要定义为volatile,因为每次对它的读写都可能有不同意义;
-
作用:在进行编译时不优化,在执行的时候不缓存, 每次都需要从内存中读出(保证内存的可见性)多用于多线程或多CPU编程。
#include <stdio.h> int main() { const int n = 10; int *p = (int*)&n; *p = 20; printf("%d\n", n); //输出10 return 0; }
#include <stdio.h> int main() { volatile const int n = 10; int *p = (int*)&n; *p = 20; printf("%d\n", n); //输出20 return 0; }
4 标准类型转换符
4.1 static_cast
-
static_cast < type-id > ( exdivssion ):编译时使用类型信息执行转换
该运算符把exdivssion转换为type-id类型,在转换执行必要的检测(诸如指针越界计算, 类型检查). 其操作数相对是安全的
-
类层次指针转换
// 类层:A -> B A *a = new A(); B *b = new B(); B *bp = static_cast<B *>(a); //基类转子类,无动态检查,不安全 A *ap = static_cast<A *>(b); //子类转基类,安全
-
基本数据类型转换,比如:enum->int、int->char...需要自己保证安全
-
把空指针转换成目标类型的空指针。
-
把任何类型的表达式转换成void类型。
-
4.2 dynamic_cast
- dynamic_cast < type-id > ( exdivssion ):运行时使用类型信息执行转换
- 该运算符把exdivssion转换成type-id类型的对象。type-id必须是类的指针、类的引用或者void *
- 如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
- dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
- 在转换时会动态检查类型,失败返回空。所以是安全的。
4.3 reinterpret_cast
- reinterpret_cast< type-id >(exdivssion):重新解释给出的对象的比特模型,而没有进行二进制转换
- type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
- 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
4.4 const_cast
-
const_cast< type-id > (exdivssion):修改类型的const或volatile属性,type_id和exdivssion的类型是一样的。
-
常量指针被转化成非常量指针,并且仍然指向原来的对象。
int a = 100; const int * a1 = &a; int *b = const_cast<int *>(a1); *b = 200;
-
常量引用被转换成非常量引用,并且仍然指向原来的对象。
int a = 100; const int & a1 = a; int &b = const_cast<int &>(a1); b = 200;
-
2 深拷贝与浅拷贝的区别
- 深拷贝(deepCopy):是增加了一个指针并且申请了一个新的内存,把对象复制了一份。
- 浅拷贝(shallowCopy):只是增加了一个指针指向已存在的内存地址。
5 重载与重写
- 重载:在同一作用域(模块/类)内,具有相同函数名(运算符名),但参数的个数、类型或返回值类型不同。重载明显是一种静态多态,在编译期间就被编译器确定。编译器会为每一个函数生成一个唯一函数标识。
- 重写(覆盖):在父类和子类中,各有一个函数名,参数信息都相同的函数。
- 静态重写:编译时根据指针的类型(父类或子类)调用相应的函数。
- 动态重写:父类的函数前加上
virtual
修饰关键字,子类重写此函数,运行时会根据实例化类型调用相应函数。
6 new和delete
6.1 new、delete、malloc、free
- new和malloc都是申请内存,但是new会调用类的构造函数;delete和free都是释放内存,但是delete会调用析构函数。
6.2 delete与 delete []区别
- delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套。
7 面向对象
- 封装:隐藏代码实现的细节。
- 继承:拓展已存在的模块。
- 多态:一个接口多种实现。
8 虚函数实现
8.1 介绍
虚函数主要是为了实现多态机制。
虚函数的实现主要由两部分组成:虚函数指针与虚函数表。
虚函数指针:每一个有虚函数的类至少有一个指针,指向自己所使用的虚函数表。这个函数指针是由编译器自动添加的,我们一般称为*__vptr
。在实例化此类时,此指针会自动被创建,并指向例化类类型的函数。
虚函数表:每一种有虚函数的类类至少有一个虚函数表,虚函数表属于类,而不属于某个具体的对象,一个类只需要一个虚表,同一个类的所有对象都使用同一个虚表。对于基类与派生类,基类有基类的虚函数表,派生类有派生类的虚函数表
8.2 基本原理
当单继承时,派生类只会存在一个虚函数指针和虚函数表。
当多继承时,派生类会存在多个虚函数指针和虚函数表。C类的虚函数和基类A共用一个虚函数表,一般称A为C的主基类。
8.3 纯虚和虚析构函数
纯虚函数:基类中只声明不定义的虚函数,提供多态实现的接口。(在很多情况下,基类本身生成对象是不合情理的,这时就要在基类中声明一个纯虚函数,将基类变为一个抽象类,就无法生成基类对象)
- 含有纯虚函数的基类是纯虚基类,不能生成对象。
- 派生类中必须对基类中声明的纯虚函数进行定义,否则无法编译。
虚析构函数:在继承关系类在释放资源时,避免只时调用了基类的析构,而导致了派生类的析构没有被调用,从而造成资源泄漏的影响。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报