这是一篇C++复习笔记。参考力扣上的收费教程 C++ 面试突破 整理而成【侵删】,基础知识比较全面。
根据我的理解对部分内容做了删减和调整,比如删掉了c++20等比较新的内容(暂时还用不到),过滤了比较老的C++语法,重难点增加了自己的理解。每段代码我都自己调试过,除了编译器导致的差异,基本没问题。
想读完整的原版内容,请移步力扣官网C++ 面试突破
#include
using namespace std;
/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var; // gp_var 在全局区(.bss 段)
int main()
{
int var; // var 在栈区
char *p_var; // p_var 在栈区
char arr[] = "abc"; // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
char *p_var1 = "123456"; // p_var1 在栈区;"123456"为字符串常量,存储在常量区
static int s_var = 0; // s_var 为静态变量,存在静态存储区(.data 段)
p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
free(p_var);
return 0;
}
栈:由系统分配,内存连续,效率高 8192
堆:开发者分配,链表状不连续,效率低
内存申请和释放,其中的技巧可能非常复杂,并且涉及许多内存分配的算法
全局变量定义在不要在头文件中定义:如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,编译时会因为重复定义而报错,因此不能再头文件中定义全局变量。一般情况下我们将变量的定义放在 .cpp 文件中,一般在 .h 文件使用extern 对变量进行声明。
我的理解:
内存对齐的思路:
(1) 对象内字段对齐。结构体第一个成员的偏移量(offset) 为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(2) 对象结尾对齐。结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
内存偏移计算
/*
说明:程序是在 64 位编译器下测试的
*/
#include
using namespace std;
#define offset(TYPE,MEMBER) ((long)&((TYPE *)0)->MEMBER)
struct A
{
short var; // 偏移 0 字节 (内存对齐原则 : short 2 字节 + 填充 2 个字节)
int var1; // 偏移 4 字节 (内存对齐原则:int 占用 4 个字节)
long var2; // 偏移 8 字节 (内存对齐原则:long 占用 8 个字节)
char var3; // 偏移 16 字节 (内存对齐原则:char 占用 1 个字节 + 填充 7 个字节)
string s; // 偏移 24 字节 (string 占用 32 个字节)
};
int main()
{
string s;
A ex1;
cout << offset(A, var) <
参考:
变量偏移计算:https://stackoverflow.com/questions/18554721/how-to-understand-size-t-type-0-member
内存对齐:https://www.bilibili.com/video/BV1Vt4y1m7DP/?spm_id_from=333.337.search-card.all.click&vd_source=7f6a092b306354c9eef8f3cd5bd5307d
内存对齐:https://zhuanlan.zhihu.com/p/30007037
当我们使用 make_share 时,我们只需要申请一块大的内存,一半用来存储资源,另一半作为管理区, 存放引用计数、用户自定的函数等,此时需要在堆上申请一次即可。
valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合。
使用 auto 关键字做类型自动推导时,依次施加以下规则:
使用 auto 关键字声明变量的类型,不能自动推导出顶层的 const 或者 volatile,也不能自动推导出引用类型,需要程序中显式声明,比如以下程序:
const int v1 = 101;
auto v2 = v1; // v2 类型是int,脱去初始化表达式的顶层const
v2 = 102; // 可赋值
int a = 100;
int &b = a;
auto c = b; // c 类型为int,脱去初始化表达式的 &
注意:编译器推导出来的类型和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。
类似于 sizeof 操作符,decltype 不对其操作数求值。decltype(e) 返回类型前,进行了如下推导:
const int&& foo();
const int bar();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1; // 类型为const int&&
decltype(bar()) x2; // 类型为int
decltype(i) x3; // 类型为int
decltype(a->x) x4; // 类型为double
decltype((a->x)) x5; // 类型为const double&
需要注意的是 lambda 函数按照值方式捕获的环境中的变量,在 lambda 函数内部是不能修改的。否则,编译器会报错。其值是 lambda 函数定义时捕获的值,不再改变。如果在 lambda 函数定义时加上 mutable 关键字,则该捕获的传值变量在 lambda 函数内部是可以修改的,对同一个 lambda 函数的随后调用也会累加影响该捕获的传值变量,但对外部被捕获的那个变量本身无影响。
#include
using namespace std;
int main()
{
size_t t = 9;
auto f = [t]() mutable{
t++;
return t;
};
cout << f() << endl; // 10
t = 100;
cout << f() << endl; // 11
cout << "t:" << t << endl; // t: 100
return 0;
}
const X 可"接住" X 和 const X;
X不能接const X
C++ 11 中允许显式地表明采用或拒用编译器提供的内置函数。
允许编译器生成默认的构造函数:
default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
禁止编译器使用类或者结构体中的某个函数:
delete 函数:= delete 修改某个函数则表示该函数不能被调用。与 default 不同的是,= delete 也能适用于非编译器内置函数,所有的成员函数都可以用 =delete 来进行修饰。
例子:
#include
using namespace std;
class A
{
public:
A() = default; // 表示使用默认的构造函数
~A() = default; // 表示使用默认的析构函数
A(const A &) = delete; // 表示类的对象禁止拷贝构造
A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
A ex1;
// 声明时初始化,调用的是拷贝构造函数
A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
A ex3;
// 之后初始化,调用的是复制运算符操作
ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
return 0;
}
A ex1–调用默认构造函数
A ex2 = ex1 – 调用拷贝构造函数
ex3 = ex1 – 调用赋值运算符操作
C++ 11 引入了新的关键字来代表空指针常量:nullptr,将空指针和整数 0 的概念拆开。nullptr 的类型为 nullptr_t
NULL会引起歧义
void f(char *);
void f(int);
char* c = NULL
f(NULL); // 会调用f(int)
nullptr 不能隐式转换为整数,也不能和整数做比较,因此就避免上述的语义歧义
nullptr是个指针,nullptr_t是一个类型
f(nullptr_t)仍然会产生歧义,可以通过显示声明一个 foo(nullptr_t) 来消除该歧义
void f(char *);
void f(int *);
void f(int);
void f(nullptr_t);
auto lambda = [](auto x, auto y) {return x + y;}
lambda(1, 2);
lambda(1.0, 2.0);
#include
#include
struct Point {
int x;
int y;
Point(int x, int y) {
this->x = x;
this->y = y;
}
};
int main() {
auto [x, y, z] = std::make_tuple(1, 2.3, "456");
auto [a, b] = std::make_pair(1, 2);
int arr[3] = {1, 2, 3};
auto [c, d, e] = arr;
auto [f, g] = Point(5, 6);
return 0;
}
增加了 any 可以存储任何类型,可以将其转化为任意类型。
std::any t = 100;
cout << std::any_cast(t) << endl;
t.reset();
t = std::string("1111111");
cout << std::any_cast(t) << endl;
strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束,而 sizeof 测量的是对象或者表达式类型占用的字节大小。strlen 源代码如下
size_t strlen(const char *str) {
size_t length = 0;
while (*str++)
++length;
return length;
}
#include
#include
using namespace std;
int main()
{
char arr[10] = "hello";
cout << strlen(arr) << endl; // 5
cout << sizeof(arr) << endl; // 10
return 0;
}
#include
#include
using namespace std;
void size_of(char arr[])
{
cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
cout << strlen(arr) << endl;
}
int main()
{
char arr[20] = "hello";
size_of(arr);
return 0;
}
/*
输出结果:
8
5
*/
二者的不同之处:
int x = 4;
char *s = "12345678";
char *p = s;
sizeof(++x);
cout << x << endl;
cout << strlen(p++) << endl; // 8
cout << strlen(p++) << endl; // 7
struct flexarray {
int val;
int array[]; /* Flexible array member; must be last element of struct */
};
int main()
{
printf("%ld\n", sizeof(struct flexarray)); // 4
}
// C++ program to demonstrate static
// variables inside a class
#include
using namespace std;
class GfG
{
public:
static int i;
GfG() {
};
};
int GfG::i = 1; // initial
int main()
{
GfG obj1;
GfG obj2;
obj1.i =2; // error
obj2.i = 3; // error
GfG::i = 10; // assignment
// prints value of i
cout << obj1.i<<" "<
// 修饰指针指向的变量
int x = 0;
int *q = &x;
const int *p = &x;
*p = 10; // error
p = q; // OK
// 修饰指针本身
int a = 8;
int* const p = &a; // 指针为常量
*p = 9; // OK
int b = 7;
p = &b; // error
// 既修饰变量,又修饰指针
int a = 8;
const int * const p = &a;
inline作用:
细节:类成员函数默认是inline, 类内的成员函数定义不用手动加inline,但若在类外定义则需要手动加
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun();
};
inline void A::fun(){
cout << var << endl;
}
C 和 C++ 对同一个函数经过编译后生成的函数名是不同的,由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的函数名中,而不仅仅是原始的函数名。比如以下函数,同一个函数 test 在 C++ 编译后符号表中生成的函数名可能为 _Z4testv,而 C 编译后符号表中生成的函数名可能为 test。
加了extern “C”,告诉编译器去找C类型的strcmp函数
// 可能出现在 C++ 头文件中的链接指示
extern "C"{
int strcmp(const char*, const char*);
}
strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖
char * strcpy(char * strDest,const char * strSrc) {
if ((NULL==strDest) || (NULL==strSrc))
throw "Invalid argument(s)";
char * strDestCopy = strDest;
while ((*strDest++=*strSrc++)!='\0');
return strDestCopy;
}
为了保证代码的健壮性和安全性,一般会使用 strncpy 代替 strcpy
#include
#include
#include
auto GetFunc(){
std::string s = "112234234234";
return [&](){ std::cout << s << std::endl; };
}
int main(int, char*[]){
auto func = GetFunc();
func();
return 0;
}
auto lambda1 = [value = 1] {return value;} //value会自动类型推断
auto lambda2 = [value = "hahahah"] {return value;};
cout << lambda1() << endl; // 1
cout << lambda2() << endl; // hahahah
用来声明类构造函数是显式调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换和赋值初始化
只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显式调用的,再加上 explicit 关键字也没有什么意义。
#include
#include
using namespace std;
class A
{
public:
int var;
A(int tmp)
{
var = tmp;
}
};
int main()
{
A ex = 10; // 发生了隐式转换,等于 A ex1(10); A ex = ex1;
return 0;
}
如果A(int tmp)前加上加上 explicit,则会编译报错
int main()
{
A ex(100);
A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
return 0;
}
默认继承权限不同
struct A{};
class B : A{}; // private 继承
struct C : B{}; // public 继承
include
using namespace std;
int main(int argc, char* argv[])
{
char buf[100];
int *p=new (buf) int(101);
cout<<*(int*)buf<
#include
class Test {
private:
int value;
public:
Test() {
printf("[Test] Constructor\n");
}
void* operator new(size_t size) {
printf("[Test] operator new\n");
return NULL;
}
};
int main()
{
Test* t = new Test();
return 0;
}
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
#include
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
return 0;
}
如果非要调用同名的父类函数可以,显示的通过父类调用
ex.Base::fun(1, 0.01);
函数声明要一模一样,包括参数和返回值。c++中重写,基类必须声明virtual
#include
using namespace std;
class Base
{
public:
virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
Base *p = new Derived();
p->fun(3); // Derived::fun(int) : 3
return 0;
}
隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。
带有虚函数的类,通过该类所隐含的虚函数表来实现多态机制,该类的每个对象均具有一个指向本类虚函数表的指针,这一点并非 C++ 标准所要求的,而是编译器所采用的内部处理方式。实际应用场景下,不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。如果涉及多重继承和虚继承,情况还将更加复杂。因此永远不要使用 C 语言的方式调用 memcpy() 之类的函数复制对象,而应该使用初始化(构造和拷构)或赋值的方式来复制对象。
#include
#include
using namespace std;
typedef void (*func)(void);
class A {
public:
void f() { cout << "A::f" << endl; }
void g() { cout << "A::g" << endl; }
void h() { cout << "A::h" << endl; }
};
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derive: public Base {
public:
void f() { cout << "Derive::f" << endl; }
void g() { cout << "Derive::g" << endl; }
void h() { cout << "Derive::h" << endl; }
};
int main()
{
Base base;
Derive derive;
//获取vptr的地址,运行在gcc x64环境下,所以将指针按unsigned long *大小处理
//另外基于C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置
unsigned long* vPtr = (unsigned long*)(&base);
//获取vTable 首个函数的地址
func vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
func vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
func vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
vPtr = (unsigned long*)(&derive);
//获取vTable 首个函数的地址
vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
cout<
对上述代码补充解释:
// &base:取虚函数表地址
// (unsigned long*): 转成long型指针。指针是long型 8个字节
unsigned long* vPtr = (unsigned long*)(&base);
//获取vTable 首个函数的地址
//*vPtr:取地址操作,取出指针指向的内容--函数表指针
//(unsigned long*):转成long型指针
//(func)*: 转成函数指针,func-前面声明的函数,则func*为函数指针
func vTable_f = (func)*(unsigned long*)(*vPtr);
有多种解释,从存储空间的角度解释比较好理解:
从存储空间的角度考虑:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
#include
using namespace std;
// 间接基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : public Base1
{
public:
int var2;
};
// 直接基类
class Base3 : public Base1
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
int main()
{
Derive d;
return 0;
}
问题:命名冲突!如上图中,base2、base3都继承了Base1的变量var1,在派生类Derive类中调用var1就发生了冲突,并不知道var1是base2的还是base3的
两种解决办法:
void set_var1(int tmp) { Base2::var1 = tmp; }
虚继承表示Base1被共享了,Base1和Base2中的变量var1实际上是同一份
#include
using namespace std;
// 间接基类,即虚基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : virtual public Base1 // 虚继承
{
public:
int var2;
};
// 直接基类
class Base3 : virtual public Base1 // 虚继承
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; }
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
int main()
{
Derive d;
return 0;
}
个人觉得,显示指定还是虚继承,要看具体的项目,如果你比较熟悉项目框架,而且逻辑不经常变,改成虚指针代码更好维护、清晰。否则,建议用显示指定,不容易出错。
class A;
A(const A& c) // 拷贝构造函数,默认实现的是浅拷贝
A c1;
A c2 = c1; //初始化类,还可以 A c2(c1);调用是拷贝构造函数
A c3;
c3 = c1; //赋值类;调用的是"=",赋值操作,默认也是浅拷贝,可以重载operate=实现深拷贝
浅拷贝有很明显的问题,如果有指针变量,会重复释放内存,抛异常
深拷贝实现如下:
#include
using namespace std;
class Test
{
private:
int *p;
public:
Test(int tmp)
{
p = new int(tmp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if (p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
Test(const Test &tmp) // 定义拷贝构造函数
{
p = new int(*tmp.p);
cout << "Test(const Test &tmp)" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1;
return 0;
}
/*
Test(int tmp)
Test(const Test &tmp)
~Test()
~Test()
*/
多继承的情况下,按顺序,第一个基类的虚函数和派生类虚函数在一个表中,其他的基类各自有一个表。总结:
看下多继承-有重写的虚函数表,稍微复杂点,稍微耐心点:
#include
using namespace std;
class Base1
{
public:
virtual void fun1() { cout << "Base1::fun1()" << endl; }
virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
public:
virtual void fun1() { cout << "Base2::fun1()" << endl; }
virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
public:
virtual void fun1() { cout << "Base3::fun1()" << endl; }
virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};
class Derive : public Base1, public Base2, public Base3
{
public:
virtual void fun1() { cout << "Derive::fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
typedef void (*func)(void);
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main(){
Base1 *p1 = new Derive();
Base2 *p2 = new Derive();
Base3 *p3 = new Derive();
p1->fun1(); // Derive::fun1()
p2->fun1(); // Derive::fun1()
p3->fun1(); // Derive::fun1()
unsigned long* vPtr = (unsigned long*)(p1);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
printVtable(vPtr, 3);
printVtable(vPtr, 4);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
cout<
这个初始化顺序是非常符合实际的逻辑的,父类、当前类的字段准备好了才能正确的对当前类构造
所以使用初始化列表,相当于减少了一次"类字段默认的初始化"
看下面例子:
#include
using namespace std;
class A
{
private:
int val;
public:
A()
{
cout << "A()" << endl;
}
A(int tmp)
{
val = tmp;
cout << "A(int " << val << ")" << endl;
}
};
class Test1
{
private:
A ex;
public:
Test1() : ex(1) // 成员列表初始化方式
{
}
};
class Test2
{
private:
A ex;
public:
Test2() // 函数体中赋值的方式
{
ex = A(2);
}
};
int main()
{
Test1 ex1;
cout << endl;
Test2 ex2;
return 0;
}
/*
运行结果:
//初始化列表
A(int 1)
//构造函数里赋值
A() // 先调类字段的默认构造函数
A(int 2) // 构造函数内调用有参构造函数
*/
例1 友元函数:
#include
using namespace std;
class A
{
friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数
public:
A(int tmp) : var(tmp)
{
}
private:
int var;
};
ostream &operator<<(ostream &_cout, const A &tmp)
{
_cout << tmp.var;
return _cout;
}
int main()
{
A ex(4);
cout << ex << endl; // 4
return 0;
}
例2 友元类
#include
using namespace std;
class A
{
friend class B;
public:
A() : var(10){}
A(int tmp) : var(tmp) {}
void fun()
{
cout << "fun():" << var << endl;
}
private:
int var;
};
class B
{
public:
B() {}
void fun()
{
cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员
}
private:
A ex;
};
int main()
{
B ex;
ex.fun(); // fun():10
return 0;
}
例 静态类型、动态类型,指针本身是静态类型,指针指向的对象是动态类型
#include
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl;
}
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()";
}
};
int main()
{
Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive*
p->fun(); // fun 是虚函数,运行阶段进行动态绑定
return 0;
}
/*
运行结果:
Derive::fun()
*/
template T max(T &a, T &b) { return a > b ? a : b; }
template
class Stack {
private:
vector elements; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elements.empty();
}
};
template
constexpr T pi = T{3.141592653589793238462643383L}; // (Almost) from std::numbers::pi
std::cout << pi << '\n';
std::cout << pi << '\n';
class Uncopyable
{
public:
Uncopyable() {}
~Uncopyable() {}
Uncopyable(const Uncopyable &) = delete; // 禁用拷贝构造函数
Uncopyable &operator=(const Uncopyable &) = delete; // 禁用赋值运算符
};
原因:避免拷贝构造函数无限制的递归而导致栈溢出。
#include
using namespace std;
class A
{
private:
int val;
public:
A(int tmp) : val(tmp) // 带参数构造函数
{
cout << "A(int tmp)" << endl;
}
A(const A &tmp) // 拷贝构造函数
{
cout << "A(const A &tmp)" << endl;
val = tmp.val;
}
A &operator=(const A &tmp) // 赋值运算符重载
{
cout << "A &operator=(const A &tmp)" << endl;
val = tmp.val;
return *this;
}
void fun(A tmp)
{
}
};
int main()
{
A ex1(1);
A ex2(2);
A ex3 = ex1;
ex2 = ex1;
ex2.fun(ex1); //传参时,会调用拷贝构造,并不是原来的变量
return 0;
}
/*
运行结果:
A(int tmp)
A(int tmp)
A(const A &tmp)
A &operator=(const A &tmp)
A(const A &tmp)
*/
const修饰的函数不允许修改类的成员,加了mutable修饰符除外
#include
using namespace std;
class A
{
public:
mutable int var1;
int var2;
A()
{
var1 = 10;
var2 = 20;
}
void fun() const // 不能在 const 修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰
{
var1 = 100; // ok
var2 = 200; // error: assignment of member 'A::var1' in read-only object
}
};
int main()
{
A ex1;
return 0;
}
看个例子, 有虚函数的继承:
/*
说明:程序是在 64 位编译器下测试的
*/
#include
using namespace std;
class A
{
private:
static int s_var; // 不影响类的大小
const int c_var; // 4 字节
int var; // 8 字节 4 + 4 (int) = 8
char var1; // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
A(int temp) : c_var(temp) {} // 不影响类的大小
~A() {} // 不影响类的大小
virtual void f() { cout << "A::f" << endl; }
virtual void g() { cout << "A::g" << endl; }
virtual void h() { cout << "A::h" << endl; } // 24 字节 12 + 4 (填充) + 8 (指向虚函数的指针) = 24
};
typedef void (*func)(void);
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main()
{
A ex1(4);
A *p;
cout << sizeof(p) << endl; // 8 字节 注意:指针所占的空间和指针指向的数据类型无关
cout << sizeof(ex1) << endl; // 24 字节
unsigned long* vPtr = (unsigned long*)(&ex1);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
return 0;
}
/*
8
24
A::f
A::g
A::h
*/
注意,虚函数表式也可能在对象内存的首地址。sizeof(ex1) 不一定是24.上面这个例子中pack == 8,如果改成4,即有效对齐值是4,8+12 = 20,不用填充了,即size为20.
修改pack size
#pragma pack(4)
查看当前pack,可以查看编译器日志,如:
xxx.cpp:416:9: warning: value of #pragma pack(show) == 8
看个例子,注意下面例子中pack为4,size = 32,如果pack为8,size=40:
#include
using namespace std; // 采用 4 字节对齐
#pragma pack(4)
class A
{
public:
int a;
};
class B : virtual public A
{
public:
int b;
void bPrintf() {
std::cout << "This is class B" << "\n";}
};
class C : virtual public A
{
public:
int c;
void cPrintf() {
std::cout << "This is class C" << "\n";}
};
class D : public B, public C
{
public:
int d;
void dPrintf() {
std::cout << "This is class D" << "\n";}
};
int main(){
A a;
B b;
C c;
D d;
cout<
总之,只要掌握对齐原则、虚基函数表原理,理解字段对齐、对象结尾对齐原则,怎么算你都错不了。