总结c++语法、内存等知识。仅供个人学习记录用
指针存放某个对象的地址,其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变
引用就是变量的别名,从一而终,不可变,必须初始化
*
符号。&
符号。什么是函数指针,如何定义和使用场景
函数指针是指向函数的指针变量。它可以用于存储函数的地址,允许在运行时动态选择要调用的函数。
格式为:返回类型 (*指针变量名)(参数列表)
int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int main() { // 定义⼀个函数指针,指向⼀个接受两个int参数、返回int的函数 int (*operationPtr)(int, int); // 初始化函数指针,使其指向 add 函数 operationPtr = &add; // 通过函数指针调⽤函数 int result = operationPtr(10, 5); cout << "Result: " << result << endl; // 将函数指针切换到 subtract 函数 operationPtr = &subtract; // 再次通过函数指针调⽤函数 result = operationPtr(10, 5); cout << "Result: " << result << endl; return 0; }
使用场景:
函数指针和指针函数的区别
函数指针是指向函数的指针变量。可以存储特定函数的地址,并在运行时动态选择要调用的函数。通常用于回调函数、动态加载库时的函数调用等场景。
int add(int a, int b) { return a + b; } int (*ptr)(int, int) = &add; // 函数指针指向 add 函数 int result = (*ptr)(3, 4); // 通过函数指针调⽤函数
指针函数是⼀个返回指针类型的函数,⽤于返回指向某种类型的数据的指针。
int* getPointer() { int x = 10; return &x; // 返回局部变ᰁ地址,不建议这样做 }
C++ 整型数据长度标准:
short 至少 16 位
int 至少与 short ⼀样长
long 至少 32 位,且至少与 int ⼀样长
long long 至少 64 位,且至少与 long ⼀样长
在使用8位字节的系统中,1 byte = 8 bit。
很多系统都使用最小长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,int 的长度较为灵活,⼀般认为 int 的长度为 4 个字节,与 long 等长。
可以通过运算符 sizeof 判断数据类型的长度。例如sizeof (int)
头文件climits定义了符号常量:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值
即为不存储负数值的整型,可以增大变量能够存储的最大值,数据长度不变。
int 被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时⼀般选用 int 类型。
关键字:static_cast、dynamic_cast、reinterpret_cast和 const_cast
static_cast
dynamic_cast
reinterpret_cast
const_cast
const的作用
const 关键字主要用于指定变量、指针、引用、成员函数等的性质
常量指针(底层const)
是指定义了一个指针,这个指针指向⼀个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是指针对其所指对象的不可改变性。
特点:靠近变量名。
形式:
(1)const 数据类型 * 指针变量 = 变量名
(2)数据类型 const * 指针变量 = 变量名
int temp = 10; const int* a = &temp; int const *a = &temp; // 更改: *a = 9; // 错误:只读对象 temp = 9; // 正确
指针常量(顶层const)
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。指针常量强调的是指针的不可改变性。
特点:靠近变量类型。
形式:数据类型 * const 指针变量 = 变量名
int temp = 10; int temp1 = 12; int* const p = &temp; // 更改: p = &temp2; // 错误 *p = 9; // 正确
拓展:
顶层const:指针本身是常量;
底层const:指针所指的对象是常量;
左定值,右定向:指的是const在*的左还是右边
const在*左边,表示不能改变指向对象的值,常量指针;
const在*右边,表示不能更换指向的对象,指针常量
若要修改const修饰的变量的值,需要加上关键字volatile;
若想要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;
static关键字主要用于控制变量和函数的生命周期、作用域以及访问权限。
实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;
void exampleFunction() { static int count = 0; // 静态变量 count++; cout << "Count: " << count << endl; }
class ExampleClass { public: static void staticFunction() { cout << "Static function" << endl; } };
class ExampleClass { public: static int staticVar; // 静态成员变量声明 }; // 静态成员变量定义 int ExampleClass::staticVar = 0;
class ExampleClass { public: static void staticMethod() { cout << "Static method" << endl; } };
void exampleFunction() { static int localVar = 0; // 静态局部变量 localVar++; cout << "LocalVar: " << localVar << endl; }
define
define:
定义预编译时处理的宏,只是简单的字符串替换,没有类型检查,不安全。
inline:
inline是先将内联函数编译完成生成了函数体,直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;
内联函数是一种特殊的函数,会进行类型检查;
对编译器的一种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
const 表示“只读”的语义,constexpr 表示“常量”的语义
constexpr 只能定义编译期常量,而const 可以定义编译期常量,也可以定义运行期常量。
你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将⼀个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。
constexpr变量
复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
必须使用常量初始化:
constexpr int n = 20; constexpr int m = n + 1; static constexpr int MOD = 1000000007;
如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。
constexpr int *p = nullptr; //常量指针 顶层const const int *q = nullptr; //指向常量的指针, 底层const int *const q = nullptr; //顶层const
constexpr函数:
constexpr函数是指能用于常量表达式的函数。
函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
constexpr int new() {return 42;}
为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。
constexpr和内联函数可以在程序中多次定义,一般定义在头文件。
constexpr 构造函数:
构造函数不能说const,但字面值常量类的构造函数可以是constexpr。
constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰
constexpr的好处
volatile是与const绝对对立的类型修饰符
影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。
作用:
指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问
使用场合:
在中断服务程序和cpu相关寄存器的定义
举例说明:
空循环:
for(volatile int i=0; i<100000; i++); // 它会执⾏,不会被优化掉
定义:声明外部变量(在函数或者文件外部定义的全局变量)
self &operator++() { //前置++ node = (linktype)((node).next); return *this; } const self operator++(int) { //后置++ self tmp = *this; ++*this; return tmp; }
为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定为⼀个0
问题:a++ 和 int a = b 在C++中是否是线程安全的?
答案:不是!
a++:
从C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,其实不是原子的。
其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的内存中
mov eax, dword ptr [a] # (1) inc eax # (2) mov dword ptr [a], eax # (3)
现在假设a的值是0,有两个线程,每个线程对变量a的值都递增1,预想⼀下,其结果应该是2,可实际运行结构可能是1!是不是很奇怪?
int a = 0; // 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3)) void thread_func1() {a++;} // 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6)) void thread_func2() {a++;}
我们预想的结果是线程1和线程2的三条指令各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1执行完指令(1)和(2)后,eax寄存器中的值变为1,此时操作系统切换到线程2执行,执行指令(3)(4)(5),此时eax的值变为1;接着操作系统切回线程1继续执⾏,执行指令(6),得到a的最终结果1。
int a = b
从C/C++语法的级别来看,这是条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址
中:
mov eax, dword ptr [b] mov dword prt [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。
解决办法
C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是⼀个模板类型:
template struct atomic:
我们可以传⼊具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型
// 初始化1 std::atomic value; value = 99; // 初始化2 // 下⾯代码在Linux平台上无法编译通过(指在gcc编译器) std::atomic value = 99; // 出错的原因是这⾏代码调⽤的是std::atomic的拷贝构造函数 // ⽽根据C++11语⾔规范,std::atomic的拷贝构造函数使⽤=delete标记禁止编译器⾃动⽣成 // g++在这条规则上遵循了C++11语言规范。
void exampleFunction() { static int count = 0; // 静态局部变ᰁ count++; cout << "Count: " << count << endl; }
int globalVar = 10; // 全局变量 void function1() { globalVar++; } void function2() { globalVar--; }
C++程序运行时,内存被分为几个不同的区域,每个区域负责不同的任务。
栈和堆都是用于存储程序数据的内存区域。
栈是一种有限的内存区域,用于存储局部变量、函数调用信息等。堆是一种动态分配的内存区域,用于存储程序运行时动态分配的数据。
栈上的变量生命周期与其所在函数的执行周期相同,而堆上的变量生命周期由程序员显式控制,可以(使用 new 或 malloc )和释放(使用 delete 或 free )。
栈上的内存分配和释放是自动的,速度较快。而堆上的内存分配和释放需要手动操作,速度相对较慢。
智能指针用于管理动态内存的对象,其主要目的是在避免内存泄漏和方便资源管理。
#include std::unique_ptr ptr = std::make_unique(42);
#include std::shared_ptr ptr1 = std::make_shared(42); std::shared_ptr ptr2 = ptr1;
#include std::shared_ptr sharedPtr = std::make_shared(42); std::weak_ptr weakPtr = sharedPtr;
1、new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
2、使用new操作符申请内存分配时无须指定内存块的大小,而malloc则需要显式地指出所需内存的尺⼨。
3、opeartor new /operator delete可以被重载,而malloc/free并不允许重载。
4、new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会,只是分配和释放内存块
5、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
6、new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
7、delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。free 不会修改指针的值,可能导致野指针问题。
8、delete 可以正确释放通过 new[] 分配的数组。free 不了解数组的大小,不适用于释放通过 malloc 分配的数组。
野指针是指指向已被释放的或无效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的行为。
int* ptr = new int; delete ptr; // 此时 ptr 成为野指针,因为它仍然指向已经被释放的内存 ptr = nullptr; // 避免野指针,应该将指针置为 nullptr 或赋予新的有效地址
int* createInt() { int x = 10; return &x; // x 是局部变量,函数结束后 x 被销毁,返回的指针成为野指针 } // 在使用返回值时可能引发未定义⾏为
void foo(int* ptr) { // 操作 ptr delete ptr; } int main() { int* ptr = new int; foo(ptr); // 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为野指针 // 避免:在 foo 函数中不要释放调用方传递的指针 }
野指针是指向已经被释放或者无效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为nullptr 或者重新分配有效的内存,导致指针仍然包含之前的内存地址。使⽤野指针进行访问会导致未定义行为,可能引发程序崩溃、数据损坏等问题。
悬浮指针是指向已经被销毁的对象的引用。当函数返回⼀个局部变量的引用,而调用者使用该引用时,就可能产生悬浮引用。访问悬浮引⽤会导致未定义行为,因为引用指向的对象已经被销毁,数据不再有效。
区别:
如何避免悬浮指针
避免在函数中返回局部变量的引用。
使用返回指针或智能指针而不是引用,如果需要在函数之外使用函数内部创建的对象。
内存对齐是指数据在内存中的存储起始地址是某个值的倍数。
在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,⽐如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果⼀个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
比如在32位cpu下,假设⼀个整型变量的地址为0x00000004(为4的倍数),那它就是自然对齐的,而如果其地址为0x00000002(非4的倍数)则是非对齐的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的⼀个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。
而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始,如果⼀个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
大多数计算机硬件要求基本数据类型的变量在内存中的地址是它们大小的倍数。例如,⼀个 32 位整数通常需要在内存中对齐到 4 字节边界。
内存对齐可以提高访问内存的速度。当数据按照硬件要求的对齐方式存储时,CPU可以更高效地访问内存,减少因为不对齐而引起的性能损失。
许多计算机体系结构使用缓存行(cache line)来从内存中加载数据到缓存中。如果数据是对齐的,那么一个缓存行可以装载更多的数据,提高缓存的命中率。
有些计算机架构要求原子性操作(比如原子性读写)必须在特定的内存地址上执行。如果数据不对齐,可能导致无法执行原子性操作,进而引发竞态条件。
用于任务链(即任务A的执行必须依赖于任务B的返回值)
常见的字符串函数实现
char str[] = "hello"; char* p = str; int n = 10; // 请计算 sizeof(str) = ? sizeof(p) = ? sizeof(n) = ? void Func(char str[100]){ // 请计算 sizeof(str) = ? } void* p = malloc(100); // 请计算 sizeof(p) = ?
参考答案:
sizeof(str) = 6;
sizeof()计算的是数组的所占内存的大小包括末尾的 ‘\0’
sizeof(p) = 4;
p为指针变量,32位系统下大小为 4 bytes
sizeof(n) = 4;
n 是整型变量,占用内存空间4个字节
void Func(char str[100]){ sizeof(str) = 4; }
函数的参数为字符数组名,即数组首元素的地址,大小为指针的大小
void* p = malloc(100); sizeof(p) = 4;
p指向malloc分配的大小为100 byte的内存的起始地址,sizeof§为指针的大小,而不是它指向内存的大小
void GetMemory1(char* p){ p = (char*)malloc(100); } void Test1(void){ char* str = NULL; GetMemory1(str); strcpy(str, "hello world"); printf(str); } char *GetMemory2(void){ char p[] = "hello world"; return p; } void Test2(void){ char *str = NULL; str = GetMemory2(); printf(str); } void GetMemory3(char** p, int num){ *p = (char*)malloc(num); } void Test3(void){ char* str = NULL; GetMemory3(&str, 100); strcpy(str, "hello"); printf(str); } void Test4(void){ char *str = (char*)malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); cout << str << endl; } }
参考答案:
Test1(void):
程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的 str一直都是NULL。strcpy(str, “hello world”)将使程序奔溃
Test2(void):
可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,使其原现的内容已经被清除,新内容不可知。
Test3(void):
能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
Test4(void):
篡改动态内存区的内容,后果难以预料。非常危险。因为 free(str);之后,str成为野指针,if(str != NULL)语句不起作用。
char* strcpy(char* strDest, const char* strSrc);
参考答案:(函数实现)
char* strcpy(char *dst,const char *src) {// [1] assert(dst != NULL && src != NULL); // [2] char *ret = dst; // [3] while ((*dst++=*src++)!='\0'); // [4] return ret; }
char s[10]="hello"; strcpy(s, s+1); // 应返回 ello strcpy(s+1, s); // 应返回 hhello 但实际会报错 // 因为dst与src重叠了,把'\0'覆盖了
所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有一种情况: src<=dst<=src+strlen(src)
C函数 memcpy 自带内存重叠检测功能,下面给出 memcpy 的实现my_memcpy
char * strcpy(char *dst,const char *src) { assert(dst != NULL && src != NULL); char *ret = dst; my_memcpy(dst, src, strlen(src)+1); return ret; } /* my_memcpy的实现如下 */ char *my_memcpy(char *dst, const char* src, int cnt) { assert(dst != NULL && src != NULL); char *ret = dst; /*内存重叠,从⾼地址开始复制*/ if (dst >= src && dst <= src+cnt-1){ dst = dst+cnt-1; src = src+cnt-1; while (cnt--){ *dst-- = *src--; } } else {//正常情况,从低地址开始复制 while (cnt--){ *dst++ = *src++; } } return ret; }
已知String的原型为:
class String { public: String(const char *str = NULL); String(const String &other); ~ String(void); String & operate =(const String &other); private: char *m_data; };
请编写上述四个函数
参考答案:
此题考察对构造函数赋值运算符实现的理解。实际考察类内含有指针的构造函数赋值运算符函数写法。
// 构造函数 String::String(const char *str) { if(str==NULL){ m_data = new char[1]; //对空字符串自动申请存放结束标志'\0' *m_data = '\0'; } else{ int length = strlen(str); m_data = new char[length + 1]; strcpy(m_data, str); } } // 析构函数 String::~String(void) { delete [] m_data; // 或delete m_data; } //拷⻉构造函数 String::String(const String &other) { int length = strlen(other.m_data); m_data = new char[length + 1]; strcpy(m_data, other.m_data); } //赋值函数 String &String::operate =(const String &other) { if(this == &other){ return *this; // 检查自赋值 } delete []m_data; // 释放原有的内存资源 int length = strlen(other.m_data); m_data = new char[length + 1]; //对m_data加NULL判断 strcpy(m_data, other.m_data); return *this; //返回本对象的引⽤ }
参考答案:
对于一个进程,其空间分布如下图所示:
如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。
参考答案:
如果是带有自定义析构函数的类类型,用new[]来创建类对象数组,而用delete来释放会发生什么?用例子来说明:
class A {}; A* pAa = new A[3]; delete pAa;
那么 delete pAa; 做了两件事:
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
无论公有继承、私有和保护继承,私有成员不能被“派生类”访问,基类中的共有和保护成员能被“派生类”访问。
对于共有继承,只有基类中的共有成员能被“派生类对象”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派生类对象”访问。
定义:让某种类型对象获得另一个类型对象的属性和方法
功能:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
常见的继承有三种方式:
1、实现继承:指使用基类的属性和方法而无需额外编码的能力
2、接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
3、可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力
例如:
将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法。
定义:数据和代码捆绑在⼀起,避免外界干扰和不确定性访问;
功能:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
定义:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
功能:多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作; 简单一句话:允许将子类类型的指针赋值给父类类型的指针。
实现多态有两种方式
C++提供了三个访问修饰符: public 、 private 和 protected 。这些修饰符决定了类中的成员对外部代码的可见性和访问权限。
public 修饰符用于指定类中的成员可以被类的外部代码访问。公有成员可以被类外部的任何代码(包括类的实例)访问。
private 修饰符用于指定类中的成员只能被类的内部代码访问。私有成员对外部代码是不可见的,只有类内部的成员函数可以访问私有成员。
protected 修饰符用于指定类中的成员可以被类的派生类访问。受保护成员对外部代码是不可见的,但可以在派生类中被访问。
一个类可以从多个基类(父类)继承属性和行为。在C++等支持多重继承的语言中,一个派生类可以同时拥有多个基类。
多重继承可能引入一些问题,如菱形继承问题, 比如当一个类同时继承了两个拥有相同基类的类,而最终的派生类又同时继承了这两个类时, 可能导致二义性和代码设计上的复杂性。为了解决这些问题,C++ 提供了虚继承, 通过在继承声明中使用 virtual 关键字,可以避免在派生类中生成多个基类的实例,从而解决了菱形继承带来的二义性。
#include class Animal { public: void eat() { std::cout << "Animal is eating." << std::endl; } }; class Mammal : public Animal { public: void breathe() { std::cout << "Mammal is breathing." << std::endl; } }; class Bird : public Animal { public: void fly() { std::cout << "Bird is flying." << std::endl; } }; // 菱形继承,同时从 Mammal 和 Bird 继承 class Bat : public Mammal, public Bird { public: void navigate() { // 这⾥可能会引起⼆义性,因为 Bat 继承了两个 Animal // navigate ⽅法中尝试调⽤ eat ⽅法,但不明确应该调⽤ Animal 的哪⼀个实现 eat(); } }; int main() { Bat bat; bat.navigate(); return 0; }
虚继承:
#include class Animal { public: void eat() { std::cout << "Animal is eating." << std::endl; } }; class Mammal : virtual public Animal { public: void breathe() { std::cout << "Mammal is breathing." << std::endl; } }; class Bird : virtual public Animal { public: void fly() { std::cout << "Bird is flying." << std::endl; } }; class Bat : public Mammal, public Bird { public: void navigate() { // 不再存在二义性,eat ⽅法来自于共享的 Animal 基类 eat(); } }; int main() { Bat bat; bat.navigate(); return 0; }
int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; }
class Base { public: virtual void print() { cout << "Base class" << endl; } }; class Derived : public Base { public: void print() override { cout << "Derived class" << endl; } };
使⽤多态是为了避免在父类里大量重载引起代码臃肿且难于维护。
重写与重载的本质区别是,加入了override的修饰符的方法,此方法始终只有一个被你使用的方法。
C++中的多态性是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针或引用上调用派生类对象的函数,以便在运行时选择正确的函数实现。
class Shape { public: virtual void draw() const { // 基类的默认实现 } };
class Circle : public Shape { public: void draw() const override { // 派⽣类的实现 } };
Shape* shapePtr = new Circle();
shapePtr->draw(); // 调⽤的是 Circle 类的 draw() 函数
class MyClass { public: int memberVariable; // 成员变量的声明 void memberFunction() { // 成员函数的实现 } };
class MyClass { public: static int staticMemberVariable; // 静态成员变量的声明 static void staticMemberFunction() { // 静态成员函数的实现 } }; int MyClass::staticMemberVariable = 0; // 静态成员变量的定义和初始化
class MyClass { public: // 默认构造函数 MyClass() { // 初始化操作 } };
class MyClass { public: // 带参数的构造函数 MyClass(int value) { // 根据参数进⾏初始化操作 } };
class MyClass { public: // 拷⻉构造函数 MyClass(const MyClass &other) { // 进⾏深拷贝或浅拷贝,根据实际情况 } };
class MyClass { public: // 委托构造函数 MyClass() : MyClass(42) { // 委托给带参数的构造函数 } MyClass(int value) { // 进⾏初始化操作 } };
C++中的虚函数的作用主要是实现了多态的机制。虚函数允许在派生类中重新定义基类中定义的函数,使得通过基类指针或引用调用的函数在运行时根据实际对象类型来确定。这样的机制被称为动态绑定或运行时多态。
在基类中,通过在函数声明前面加上 virtual 关键字,可以将其声明为虚函数。派生类可以重新定义虚函数,如果派生类不重新定义,则会使用基类中的实现。
class Base { public: virtual void virtualFunction() { // 虚函数的实现 } }; class Derived : public Base { public: void virtualFunction() override { // 派⽣类中对虚函数的重新定义 } };
虚函数的实现通常依赖于一个被称为虚函数表(虚表)的数据结构。每个类(包括抽象类)都有一个虚表,其中包含了该类的虚函数的地址。每个对象都包含一个指向其类的虚表的指针,这个指针被称为虚指针(vptr)。
当调用一个虚函数时,编译器会使用对象的虚指针查找虚表,并通过虚表中的函数地址来执行相应的虚函数。这就是为什么在运行时可以根据实际对象类型来确定调用哪个函数的原因。
class Base { public: // 虚函数有实现 virtual void virtualFunction() { // 具体实现 } };
class AbstractBase { public: // 纯虚函数,没有具体实现 virtual void pureVirtualFunction() = 0; // 普通成员函数可以有具体实现 void commonFunction() { // 具体实现 } };
抽象类是不能被实例化的类,它存在的主要目的是为了提供一个接口,供派生类继承和实现。抽象类中可以包含普通的成员函数、数据成员和构造函数,但它必须包含至少一个纯虚函数。即在声明中使用 virtual 关键字并赋予函数一个 = 0 的纯虚函数。
纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。通过在函数声明的末尾使用 = 0 ,可以将虚函数声明为纯虚函数。派生类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类。
class AbstractShape { public: // 纯虚函数,提供接⼝ virtual void draw() const = 0; // 普通成员函数 void commonFunction() { // 具体实现 } };
虚析构函数是一个带有 virtual 关键字的析构函数。 主要作用是确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放对象所占用的资源。
通常,如果一个类可能被继承,且在其派生类中有可能使用 delete 运算符来删除通过基类指针指向的对象,那么该基类的析构函数应该声明为虚析构函数。
class Base { public: // 虚析构函数 virtual ~Base() { // 基类析构函数的实现 } }; class Derived : public Base { public: // 派⽣类析构函数,可以覆盖基类的虚析构函数 ~Derived() override { // 派⽣类析构函数的实现 } };
虚析构函数允许在运行时根据对象的实际类型调用正确的析构函数,从而实现多态性。
如果基类的析构函数不是虚的,当通过基类指针删除指向派生类对象的对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类的资源未被正确释放,造成内存泄漏。
构造函数在对象的创建阶段被调用,对象的类型在构造函数中已经确定。因此,构造函数调用不涉及多态性,也就是说,在对象的构造期间无法实现动态绑定。虚构造函数没有意义,因为对象的类型在构造过程中就已经确定,不需要动态地选择构造函数。
class Base { public: // 错误!不能声明虚构造函数 virtual Base() { // 虚构造函数的实现 } virtual ~Base() { // 基类析构函数的实现 } };
常见的不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。