c++进阶知识点复习(1)

这是一篇C++复习笔记。参考力扣上的收费教程 C++ 面试突破 整理而成【侵删】,基础知识比较全面。

根据我的理解对部分内容做了删减和调整,比如删掉了c++20等比较新的内容(暂时还用不到),过滤了比较老的C++语法,重难点增加了自己的理解。每段代码我都自己调试过,除了编译器导致的差异,基本没问题。

想读完整的原版内容,请移步力扣官网C++ 面试突破

c++ 编译与内存相关


#include <iostream>
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. 硬件平台,只能读取特定的字节数。CPU 访问内存时并不是以字节为单位来读取内存,而是以机器字长为单位
  2. 效率,避免多次读取+拼接
  3. 原子性,避免一块数据分多次读取

内存对齐的思路:

  • (1) 对象内字段对齐。结构体第一个成员的偏移量(offset) 为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

  • (2) 对象结尾对齐。结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

内存偏移计算

/*
 说明:程序是在 64 位编译器下测试的
 */
 #include <iostream>
 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) <<endl;
     cout << offset(A, var1) <<endl;
     cout << offset(A, var2) <<endl;
     cout << offset(A, var3) <<endl;
     cout << offset(A, s) <<endl;
     cout << sizeof(ex1) << endl;  // 56 struct
     return 0;
 }

参考:

变量偏移计算: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

weak_ptr

  • 指向 share_ptr 指向的对象,能够解决由 shared_ptr 带来的循环引用问题。
  • 与 shared_ptr 配合使用,将 weak_ptr 转换为 share_ptr 时,虽然它能访问 share_ptr 所指向的资源但却不享有资源的所有权,不影响该资源的引用计数。
  • 有可能资源已被释放,但 weak_ptr 仍然存在,share_ptr本身必须等待所有引用的 weak_ptr 全部被释放才会进行释放。因此每次访问资源时都需要判断资源是否有效。

智能指针的创建细节

当我们使用 make_share 时,我们只需要申请一块大的内存,一半用来存储资源,另一半作为管理区, 存放引用计数、用户自定的函数等,此时需要在堆上申请一次即可。

编译与链接

内存泄漏检测

valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合。

C++语言特性

c++11特性

  1. 类型推导 auto decltype

使用 auto 关键字做类型自动推导时,依次施加以下规则:

  • 首先,如果初始化表达式是引用,首先去除引用;
  • 上一步后,如果剩下的初始化表达式有顶层的 const 或 volatile 限定符,去除掉。

使用 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) 返回类型前,进行了如下推导:

  • 若表达式 e 为一个无括号的变量、函数参数、类成员访问,那么返回类型即为该变量或参数或类成员在源程序中的“声明类型”;
  • 否则的话,根据表达式的值分类(value categories),设 T 为 e 的类型:
    • 若 e 是一个左值(lvalue,即“可寻址值”),则 decltype(e) 将返回T&;
    • 若 e 是一个临终值(xvalue),则返回值为 T&& ;若 e 是一个纯右值(prvalue),则返回值为 T。
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&
  1. lambda表达式

需要注意的是 lambda 函数按照值方式捕获的环境中的变量,在 lambda 函数内部是不能修改的。否则,编译器会报错。其值是 lambda 函数定义时捕获的值,不再改变。如果在 lambda 函数定义时加上 mutable 关键字,则该捕获的传值变量在 lambda 函数内部是可以修改的,对同一个 lambda 函数的随后调用也会累加影响该捕获的传值变量,但对外部被捕获的那个变量本身无影响。

#include <iostream> 
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;
}
  1. 左值 & 右值
  • 引用绑定规则如下:
    • 非常量左值引用(X &):只能绑定到 X 类型的左值对象;
    • 常量左值引用(const X &):可以绑定到 X、const X 类型的左值对象,或 X、const X 类型的右值;
    • 非常量右值引用(X &&):只能绑定到 X 类型的右值;
    • 常量右值引用(const X &&):可以绑定规定到 X、const X 类型的右值。

const X 可"接住" X 和 const X;
X不能接const X

  1. default、delet

C++ 11 中允许显式地表明采用或拒用编译器提供的内置函数。

允许编译器生成默认的构造函数:

default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。

禁止编译器使用类或者结构体中的某个函数:

delete 函数:= delete 修改某个函数则表示该函数不能被调用。与 default 不同的是,= delete 也能适用于非编译器内置函数,所有的成员函数都可以用 =delete 来进行修饰。

例子:

#include <iostream>
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 – 调用赋值运算符操作

  1. nullptr VS NULL

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);

c++14 c++17新特性

  1. 在 C++ 14 中,lambda 函数的形式参数允许泛型
auto lambda = [](auto x, auto y) {return x + y;}
lambda(1, 2);
lambda(1.0, 2.0);
  1. C++ 17 新特性
  • 结构化绑定
    利用该特性可以把以 C++ 中的 pair,tuple,array,struct 的成员赋值给多个变量。
#include <iostream>
#include <tuple>

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;
}
  • std::any

增加了 any 可以存储任何类型,可以将其转化为任意类型。

std::any t = 100;
cout << std::any_cast<int>(t) << endl;
t.reset();
t = std::string("1111111");
cout << std::any_cast<string>(t) << endl;

C++关键字与关键库函数

sizeof 和 strlen 的区别

  • strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。

strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束,而 sizeof 测量的是对象或者表达式类型占用的字节大小。strlen 源代码如下

size_t strlen(const char *str) {
    size_t length = 0;
    while (*str++)
        ++length;
    return length;
}
  
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    char arr[10] = "hello";
    cout << strlen(arr) << endl; // 5
    cout << sizeof(arr) << endl; // 10
    return 0;
} 
  • 若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。
  #include <iostream>
#include <cstring>

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
*/
  • 二者的不同之处:

    • strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 是在编译时计算长度;sizeof 的参数可以是类型,也可以是变量,且必须是完整类型;strlen 的参数必须是 char * 类型的变量。
    • sizeof 接受的参数可以是对象也可以是表达式,但是 sizeof(expression) 在运行时不会对接受的表达式进行计算,编译器只会推导表达式的类型从而计算占用的字节大小;而 strlen 是一个函数,如果接受表达式则会对表达式进行运算。
int x = 4;
char *s = "12345678";
char *p = s;
sizeof(++x);
cout << x << endl;
cout << strlen(p++) << endl; // 8
cout << strlen(p++) << endl; // 7
  • 对于 C99 中结构体允许最后一个变量为不定长数组,sizeof 则不计算空间。
struct flexarray {
  int val;
  int array[];  /* Flexible array member; must be last element of struct */
};
  
int main()
{
    printf("%ld\n", sizeof(struct flexarray)); // 4
}

static

  • 全局静态变量,作用域只在文件内,在其他文件中extern 声明会报错找不到。全局变量可以。
  • 全局静态函数的作用域同全局静态变量
  • 类静态变量声明和初始化要分开,声明在类中,初始化在类外,且要去掉 static 关键字和 private、public、protected访问规则
  // C++ program to demonstrate static
// variables inside a class
#include<iostream>
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<<" "<<obj2.i; // 10 
}

const 作用及用法

  • const 指针
    • const 修饰指针指向的内容.则指针指向的内容不可变,但是指针本身的内容可以改变。
    • const 修饰指针.则指针为不可变量,指针指向的内容可以变,但指针本身不能变。
    • const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
// 修饰指针指向的变量
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, 类内的成员函数定义不用手动加inline,但若在类外定义则需要手动加

  class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }
    void fun();
};

inline void A::fun(){
    cout << var << endl;
}

new malloc

  • new是c++操作符,malloc是c的函数
  • new除了调用malloc还会调用对象的构造函数,对内存进行初始化
  • new 返回对象的指针类型,malloc返回void *
  • new 失败抛出异常bad_alloc, malloc返回NULL
  • new 由编译器自动计算空间,malloc需手动指定
  • new 作为运算符可以重载,malloc作为c函数不支持重载
  • malloc 可以更改空间,realloc, new不能更改

volatile作用

  • 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。可用于多线程状态同步。
  • 阻止编译器调整操作 volatile 变量的指令排序。硬件驱动需要按要求逐步初始化。

struct

  • c++中 class可以实现struct的所有功能,为了兼容c保留struct, c++中struct可以继承,也可以实现多态
  • c中,struct没有权限的设置,只是单纯的数据的封装,c++中struct默认是public权限
  • c中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var

extern “C” 作用-Android中Jni经常看到

C 和 C++ 对同一个函数经过编译后生成的函数名是不同的,由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的函数名中,而不仅仅是原始的函数名。比如以下函数,同一个函数 test 在 C++ 编译后符号表中生成的函数名可能为 _Z4testv,而 C 编译后符号表中生成的函数名可能为 test。

加了extern “C”,告诉编译器去找C类型的strcmp函数

  // 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
    int strcmp(const char*, const char*);
}

strcpy 函数的缺陷

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

lambda

  1. 引用捕获可能带来悬挂引用常见于使用 lambda 表达式使用引用捕获某个局部变量,而调用 lambda 表达式时,局部变量已经被清理导致捕获的引用指向被清理的内存空间,从而产生悬挂引用。比如下面程序实例中,当 GetFunc 返回时,s 的对象已经被销毁,此时 s 的引用则会出现问题,应将其修改为值传递。
#include <iostream>
#include <cstring>
#include <functional>

auto GetFunc(){
    std::string s = "112234234234";
    return [&](){ std::cout << s << std::endl; };
}

int main(int, char*[]){
    auto func = GetFunc();
    func();
    return 0;
}
  1. 在 C++ 14 以后,lambda 函数的形式参数允许泛型和初始化捕获。
  auto lambda1 = [value = 1] {return value;} //value会自动类型推断
  auto lambda2 = [value = "hahahah"] {return value;};
  cout << lambda1() << endl; // 1
  cout << lambda2() << endl; // hahahah

explicit的作用

用来声明类构造函数是显式调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换和赋值初始化

只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显式调用的,再加上 explicit 关键字也没有什么意义。

  #include <iostream>
#include <cstring>
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;
}

class和struct的异同

默认继承权限不同

struct A{};
class B : A{}; // private 继承 
struct C : B{}; // public 继承

new

  1. 对于指定的地址的 new 对象
  include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    char buf[100];
    int *p=new (buf) int(101);
    cout<<*(int*)buf<<endl; // 100
    return 0;
}

  1. new 重载
  • 全局重载,所有调用new的地方都会生效
  • 类局部重载,仅针对重载的类生效
  #include <iostream>

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;
}

面向对象

重载、重写、隐藏

  1. 重载不关心返回值,签名只包含函数名、参数
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 错误:注意重载不关心函数返回类型
};
  1. 隐藏,继承类会隐藏同名的父类函数,不区别参数的差异
#include <iostream>
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);
  1. 重写(覆盖overload)

函数声明要一模一样,包括参数和返回值。c++中重写,基类必须声明virtual

#include <iostream>
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;
}

隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。

  1. 虚函数和纯虚函数
  • 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
  • 可以声明抽象类指针,可以声明抽象类的引用;
  • 纯虚函数定义时除了加上 virtual 关键字还需要加上 =0,如果不加=0,基类必须实现该函数,否则会报错
  1. 虚函数实现机制
  • 虚函数表存放的内容:类的虚函数的地址。
  • 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。有些资料说可能放到对象内存末尾,由编译器实现
  • 虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable

带有虚函数的类,通过该类所隐含的虚函数表来实现多态机制,该类的每个对象均具有一个指向本类虚函数表的指针,这一点并非 C++ 标准所要求的,而是编译器所采用的内部处理方式。实际应用场景下,不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。如果涉及多重继承和虚继承,情况还将更加复杂。因此永远不要使用 C 语言的方式调用 memcpy() 之类的函数复制对象,而应该使用初始化(构造和拷构)或赋值的方式来复制对象。

  1. 手动获取虚函数表,并调用虚函数
  #include <iostream>
#include <memory>
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<<sizeof(A)<<endl; // 无字段的类对象至少占用一个字节,用唯一的地址以区分
    cout<<sizeof(base)<<endl; // 虚标指针,一个指针8个字节 long *类型
    cout<<sizeof(derive)<<endl; // 虚标指针,一个指针8个字节 long *类型
	return 0;
}
/*
Base::f
Base::g
Base::h
Derive::f
Derive::g
Derive::h
1
8
8
*/

对上述代码补充解释:

  // &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);
  1. 构造函数一般不定义为虚函数

有多种解释,从存储空间的角度解释比较好理解:

从存储空间的角度考虑:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。

  1. 多重继承(菱形继承)的问题

  #include <iostream>
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 <iostream>
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 <iostream>

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 <iostream>
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<<sizeof(Base1)<<endl; // 8
	cout<<sizeof(Base2)<<endl; // 8
	cout<<sizeof(Base3)<<endl; // 8
    cout<<sizeof(Derive)<<endl; // 8
    return 0;
}

/*
Derive::fun1()
Derive::fun1()
Derive::fun1()
Derive::fun1()
Base1::B1_fun2()
Base1::B1_fun3()
Derive::D_fun2()
Derive::D_fun3()
Derive::fun1()
Base2::B2_fun2()
Base2::B2_fun3()
Derive::fun1()
Base3::B3_fun2()
Base3::B3_fun3()
8
8
8
24
*/

类对象的初始化顺序

  1. 按继承顺序调用基类构造函数(虚继承的类优先)
  2. 类字段构造–>调各自的类构造函数
  3. 当前类的构造函数
  4. 析构,析构和构造的顺序相反

这个初始化顺序是非常符合实际的逻辑的,父类、当前类的字段准备好了才能正确的对当前类构造

成员初始化列表效率高的原因

  • 按正常的执行顺序:类字段默认初始化->类构造函数->类字段赋值
  • 如果有初始化列表:类初始化列表–>类构造函数

所以使用初始化列表,相当于减少了一次"类字段默认的初始化"

看下面例子:

  #include <iostream>
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. 函数fun声明为类A的友元,则fun能调用A的private、protected字段
  2. 类B定义为类A的友元,则类B可以可以访问A的private、protected字段

例1 友元函数:

  #include <iostream>

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 <iostream>

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;
}

静态绑定和动态绑定

  1. 静态绑定&动态绑定
  • 变量声明时的类型,为静态类型,编译器决定了
  • 变量运行时所指对象的类型,为动态类型,运行期决定
  • 类中,只有虚函数是动态绑定,其余的都是静态绑定

例 静态类型、动态类型,指针本身是静态类型,指针指向的对象是动态类型

#include <iostream>

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()
*/
  1. 继承中的虚函数排列
    c -> 继承 B -> 继承 A
    继承中的虚函数排列
  • 同名虚函数在基类和派生类中的虚函数表中,在虚函数表中偏移位置是一致的.图 A,B,C 的 display 的偏移位置都为 0。只有这样才能保证动态绑定->虚函数调用是按照偏移来查找的,因为不管是哪个子类,基类的虚函数偏移都是一样的。
  • 如果派生类中定义了与基类同名的虚函数,那么派生类的虚函数表中响应函数的入口地址会被替换成覆盖后的函数的地址。编译期修改的。
  • 虚函数表中先放基类的,再放子类的(除非有同名的发生重写覆盖),所以子类的虚函数会依次放到末尾

c++ 模板

  1. 函数模板
  template<typename T> T max(T &a, T &b) { return a > b ? a : b; }
  1. 类模板
template <class T>
class Stack { 
  private: 
    vector<T> elements;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elements.empty(); 
    } 
}; 
  1. C++14 变量模板
    实现不同精度的PI
template<typename T> 
constexpr T pi = T{3.141592653589793238462643383L}; // (Almost) from std::numbers::pi

std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n'; 

禁止拷贝

  1. 将拷贝构造函数和operate= 设置为private
  2. c++ 11支持delete,推荐用delete
  class Uncopyable
{
public:
    Uncopyable() {}
    ~Uncopyable() {}
     Uncopyable(const Uncopyable &) = delete;            // 禁用拷贝构造函数
     Uncopyable &operator=(const Uncopyable &) = delete; // 禁用赋值运算符
};

为什么拷贝构造函数必须声明为引用

原因:避免拷贝构造函数无限制的递归而导致栈溢出。

#include <iostream>
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

const修饰的函数不允许修改类的成员,加了mutable修饰符除外

  #include <iostream>

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;
}

类的大小

  1. 复习下对齐原则
  • 对齐系数,pack编译器默认是8个字节,和类字段最宽值取较大值,称为有效对齐值
  • 每次对齐,以min(当前字段宽度,有效对齐值)的倍数计数
  • 类结束,以有效对齐值对齐

看个例子, 有虚函数的继承:

/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>

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
  1. 带虚继承的内存占用
    虚继承是为了解决菱形继承,产生基类字段的重复,会产生虚基函数表(vptr)
    原理参考:https://blog.csdn.net/xiejingfa/article/details/48028491

看个例子,注意下面例子中pack为4,size = 32,如果pack为8,size=40:
虚继承

  #include <iostream>
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<<sizeof(a)<<endl;
    cout<<sizeof(b)<<endl;
    cout<<sizeof(c)<<endl;
    cout<<sizeof(d)<<endl;
    return 0;
}
/*
4
16
16
32
*/

总之,只要掌握对齐原则、虚基函数表原理,理解字段对齐、对象结尾对齐原则,怎么算你都错不了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值