HOME
BLOG
Cplusplus部分总结
May 22 2023

记录我下班重温cpp的一些笔记。

[TOC]

new 重载 运算符 返回具体类型 抛异常 不用指定内存大小 多构造对象的过程
malloc 指定内存大小 void* 返回null 
二维数组分配内存
int(*arr)[5]=new int[4][5];//直接全分配好

int **brr=new int*[4];//同下面malloc一个意思
brr[i]=new int[3];

int **crr=(int**)malloc(sizeof(int*)*2);//2行的指针数组
crr[i]=(int*)malloc(sizeof(int)*3);//具体每行的3个元素

this-> classname *const this

const成员函数中 const classname *const this ,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);

this是右值,所以不能取this的地址

在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数


虚函数可以是内联函数吗?

可以,但是当虚函数表现出多态性就不能内联

多态-》运行期,,内联-》编译期。编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。

inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。


volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。

所以volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)

  • const 可以是 volatile (如只读的状态寄存器)
  • 指针可以是 volatile
  • volatile多用在多线程

union任意时刻只有一个数据成员可以持有值

  1. 默认访问控制符为 public
  2. 可以含有构造函数、析构函数
  3. 不能含有引用类型的成员
  4. 不能继承自其他类,不能作为基类
  5. 不能含有虚函数
  6. 匿名 union 在定义所在作用域可直接访问 union 成员
  7. 匿名 union 不能包含 protected 成员或 private 成员
  8. 全局匿名联合必须是静态(static)的
union unionTest
{
   unionTest():i(10){
      cout<<"unionTest i"<<endl;
   }
   int i;
   double d;
};
//全局匿名的
static union
{

   int i;
   double d;
};
int main()
{
   unionTest myunion;
   
   //局部匿名union
   union{
      int i;
      double d;
   };

   cout<<myunion.i<<endl;

   ::i=20;
   cout<<::i<<endl;// 输出全局静态匿名联合的 20

   i=30;
   cout<<i<<endl;// 输出局部匿名联合的 30
}

explicit 修饰构造函数时,可以防止隐式转换和复制初始化

explicit 修饰转换函数时,可以防止隐式转换

struct A
{
    A(int) {}
    operator bool() const { return true; }
};

struct B
{
    explicit B(int) {}
    explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
    A a1(1);     // OK:直接初始化
    A a2 = 1;    // OK:复制初始化
    A a3{1};     // OK:直接列表初始化
    A a4 = {1};  // OK:复制列表初始化
    A a5 = (A)1; // OK:允许 static_cast 的显式转换
    doA(1);      // OK:允许从 int 到 A 的隐式转换
    if (a1);                            // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a6(a1);                   // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a7 = a1;                    // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化

    B b1(1);     // OK:直接初始化
    B b2 = 1;    // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
    B b3{1};     // OK:直接列表初始化
    B b4 = {1};  // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
    B b5 = (B)1; // OK:允许 static_cast 的显式转换
    doB(1);      // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
    if (b1);                            // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b6(b1);                     // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b7 = b1;                    // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
    bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化

    return 0;
}

c++11之后使用using可以用来声明继承构造函数

class Base
{
public:
    Base(int va) :m_value(va), m_c('0') {}
    Base(char c) :m_c(c), m_value(0) {}
private:
    int m_value;
    char m_c;
};
 
class Derived :public Base
{
public:
    //使用继承构造函数
    using Base::Base;
 
    //假设派生类只是添加了一个普通的函数
    void display()
    {
//dosomething		
    }
};

https://blog.csdn.net/SwordArcher/article/details/88717442 using在继承构造函数中的用法(c++11)


c++的多态

  • C++ 多态分类及实现:
    1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
    2. 子类型多态(Subtype Polymorphism,运行期):虚函数
    3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
    4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的。静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早绑定。静态多态往往也被叫做静态联编。 动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定,动态多态往往也被叫做动太联编

编译期是指把你的源程序交给编译器编译的过程,最终目的是得到obj文件,链接后生成可执行文件(预处理、编译、汇编和链接)。 运行期指的是你将可执行文件交给操作系统(输入文件名,回车)执行、直到程序执行结束


模板编译与链接

现在就来看看,编译器对模板是如何编译和链接吧(https://www.cnblogs.com/jinxiang1224/p/8468272.html)

当编译器遇到一个template时,不能够立马为他产生机器代码,它必须等到template被指定某种类型。也就是说,函数模板和类模板的完整定义将出现在template被使用的每一个角落,比如遇到上述中的4个语句时,才能确定编译内容,否则编译器没有足够的信息产生机器代码。

对于不同的编译器,其对模板的编译和链接技术也会有所不同,其中一个常用的技术称之为Smart,其基本原理如下:

  1. 模板编译时,以每个cpp文件为编译单位,实例化该文件中的函数模板和类模板

  2. 链接器在链接每个目标文件时,会检测是否存在相同的实例;有存在相同的实例版本,则删除一个重复的实例,保证模板实例化没有重复存在。

比如我们有一个程序,包含A.cpp和B.cpp,它们都调用了CThree模板类,在A文件中定义了int和double型的模板类,在B文件中定义了int和float型的模板类;在编译器编译时.cpp文件为编译基础,生成A.obj和B.obj目标文件,即使A.obj和B.obj存在重复的实例版本,但是在链接时,链接器会把所有冗余的模板实例代码删除,保证exe中的实例都是唯一的。编译原理和链接原理,如下所示:


问为什么虚继承能解决菱形继承的问题???

虚继承底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚继承、虚函数

虚继承用的指针是vbptr,,虚函数的是vfptr

​ 所谓虚函数表,是编译器自动为一个带有虚函数的类生成的一块内存空间,其中存储着每一个虚函数的入口地址。由于函数的入口地址可以看成一个指针类型,因此这些虚函数的地址间隔为四个字节。而每一个带有虚函数类的实例,都拥有一个虚函数指针——vptr,在类的对象初始化完毕后,它将指向虚函数表。

虚函数表、虚函数指针是编译期确定的,但是虚函数表里面的虚函数具体的实现方式是运行期确定的。

  • 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同之处:
    • 虚继承
      • 虚基类依旧存在继承类中,只占用存储空间
      • 虚基类表存储的是虚基类相对直接继承类的偏移量,通过偏移量的指向去解决菱形继承的问题
    • 虚函数
      • 虚函数不占用存储空间
      • 虚函数表存储的是虚函数地址

常用的一些宏,日志,调试经常用

编译器内置宏:

ANSI C标准中有几个标准预定义宏(也是常用的):

  • LINE:在源代码中插入当前源代码行号;
  • FILE:在源文件中插入当前源文件名;
  • DATE:在源文件中插入当前的编译日期
  • TIME:在源文件中插入当前编译时间;
  • STDC:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
  • __cplusplus:当编写C++程序时该标识符被定义,例如__cplusplus199711,__cplusplus201103。
  • FUNCTION:调用函数的名称

编译器在进行源码编译的时候,会自动将这些宏替换为相应内容。

标识符__LINE__和__FILE__通常用来调试程序;

标识符__DATE__和__TIME__通常用来在编译后的程序中加入一个时间标志,以区分程序的不同版本;

这四个都是预编译宏,不是包含在头文件中的:

  • __FILE__是当前编译的文件的文件名 是一个字符串
  • __TIME__是当前编译的文件的编译时间 格式是hh:mm:ss 是字符串
  • __DATE__是当前编译的文件的编译日期 格式是Mmm:dd:yyyy 是字符串
  • __LINE__是调用该宏语句所在的行数,是个十进制数
#include<iostream>
using namespace std;
int main()
{
   cout<<"line:"<<__LINE__<<endl;
   cout<<"file:"<<__FILE__<<endl;
   cout<<"date:"<<__DATE__<<endl;
   cout<<"time:"<<__TIME__<<endl;
   cout<<"version:"<<__cplusplus<<endl;
   cout<<"function:"<<__FUNCTION__<<endl;
}

进程之间私有和共享的资源

  • 私有:地址空间、堆、全局变量、栈、寄存器
  • 共享:代码段,公共数据,进程目录,进程 ID

线程之间私有和共享的资源

  • 私有:线程栈,寄存器,程序计数器
  • 共享:堆,地址空间,全局变量,静态变量

有关继承,你要知道

  • 派生类从继承可以继承来所有的成员(变量和方法),析构,友元类,函数不会继承,,同名的函数和变量不会继承,相当于隐藏了,需要自己手动调用

有的说没有继承来父类的构造和析构,,怎么说呢,个人认为其实是继承下来了,如果没有继承下来,在派生类里面怎么调用父类的构造去初始化继承来的父类的成员呢?

复盘,构造不能被继承,初始化列表那种写法是显示调用父类构造

查询C++primer 15.7.4得到:类不会继承默认、拷贝、移动构造函数,如果派生类没有直接定义这些构造函数,编译器会自动为派生类合成这些构造

下面是gpt4的回答:

在C++中,派生类不能继承基类的构造函数和析构函数。这是因为构造函数和析构函数是特殊的成员函数,它们在对象的创建和销毁时自动调用,而不是由派生类继承。特殊的那些函数都不能继承

C++ Primer第五版中第15章第2节中有明确的说明:

派生类不继承基类的构造函数或析构函数。一个派生类可以调用基类的构造函数,但是不能继承它。同样,一个派生类可以调用基类的析构函数,但是不能继承它。

引入-》C++11中使用using继承父类构造函数的独特用法

class A
{
private:
    int data;
protected:
    A(int a):data(a){
        cout<<"A()"<<endl;
    }
    ~A(){cout<<"~A()"<<endl;}
};

class B:public A
{
private:
    int data1;
public:
    B(int a):A(a),data1(a)  //是 显示调用父类构造
    {
        cout<<"B()"<<endl;
    }
    ~B(){cout<<"~B()"<<endl;}
};
int main(void)
{
    B b(20);

    system("pause");
    return 0;
}
  • 派生类要想初始化继承来的父类的成员,需要调用基类相应的构造函数来初始化
  • 派生类的构造函数和析构,负责初始化和清理派生类的成员
  • 基类的构造和析构,去负责派生类继承来的部分

如果B私有继承了A,那么A里面的public和proected相对于B都成了private,B只能在他内部去访问继承下来的这些相对于他成了private的成员,

如果A里面有私有成员,在私有继承的情况下(无论什么继承情况下),父类的私有成员只能被父类本身访问和使用,而不能被派生类或其他外部类访问和使用。因此B去继承A,A的私有东西B根本无法访问的

存疑,无法访问是ok的,但是A的私有属性是否会被继承下来? 个人认为会被继承,但是无法访问

查询gpt结论是私有成员会被继承,我写了测试demo看内存地址也是存在的

作用域

对于派生类和基类同名函数,参数也相同,,这种是可以的,毕竟作用域不相同。这种就叫隐藏,派生类对象想要去调和他同名的父类方法,就需要显示去调用了

虚析构

当基类指针或引用指向从堆上构建的派生类对象时,基类析构必须要写成虚的

{
    Base *de=new Devire();
    delete de;
    de=nullptr;
    return 0;
}

虚函数一定是动态绑定吗?

不是的,只要是对象本身去调用,即使函数写的虚函数,但还是静态绑定。动态绑定也没必要,你动态绑定最后访问的不还是自己的对应的虚函数嘛

其次你在构造函数里面去调用虚函数,发生的也是静态绑定,不会发生动态绑定

一道笔试题:为什么结果是Devire:10 ,动态绑定调用到Devire没问题,可是i 为什么是10呢?

因为函数参数压栈这些都是在程序编译阶段,编译阶段编译器只能看到是Base指针调用show,因此i压栈为10,而动态绑定在运行阶段,运行期间发现show是虚函数,且Base指针指向的是Devire对象,所以才会去Devire里调show,因此打印的是Devire,但是i在编译时已经压栈是10了,那么打印出来的i就是10,并不是i=20这个默认值,他毕竟没有被压到栈里

class Base
{
public:
    Base(){
        cout<<"BASe"<<endl;
    }
    virtual ~Base(){
        cout<<"---BASe"<<endl;
    }
    virtual void show(int i=10)
    {
        cout<<"BAse:"<<i<<endl;
    }
 
};
class Devire:public Base
{
public:
    Devire(){
        cout<<"Devire"<<endl;
    }
     ~Devire(){
        cout<<"---Devire"<<endl;
    }
    virtual void show(int i=20)
    {
        cout<<"Devire:"<<i<<endl;
    }
};
int main()
{
    Base *de=new Devire();
    de->show();
    delete de;
    de=nullptr;
    return 0;
}

上面代码稍微修改一下,把派生类的show方法改为私有,结果还是跟上的一样,思考为什么?

class Devire:public Base
{
public:
    Devire(){
        cout<<"Devire"<<endl;
    }
     ~Devire(){
        cout<<"---Devire"<<endl;
    }
private:    //改为私有的了
    void show(int i=20)
    {
        cout<<"Devire:"<<i<<endl;
    }
};

要分清什么事情在编译阶段做的,什么事情在运行阶段做的,,

访问权限是在编译阶段确定的,,只有编译链接完成了才能去运行。在编译阶段编译器看到的还是Base对象去调用show方法,且i=10压栈,他的show是公有的,此时代码的访问权限已经被确定了,因此编译是没问题的。。但是到了运行期间,编译器看到指针指的是派生类,就会通过虚表指针找到对应的虚函数去调用,他才不管你什么访问权限呢,因为你编译都通过了说明访问权限啥的代码检查是没问题的

虚继承的问题

当b虚继承a之后,b类型就会多一个vbptr的指针。当虚继承之后,虚基类的数据就会放在派生类的最下面(原本正常继承来说,继承的东西在上面),然后派生类最上面加一个vbptr。

当虚继承+虚函数是ok的,可以正常实现多态,但是当遇到虚基类指针/引用指向派生类对象(堆上new的)的时候,用完需要再去delete虚基类指针,内存回收就会出问题。。把上面的例子改成虚继承,可以试试

知道答案之前,先要记住:基类指针指向派生类对象,永远指向的是派生类基类部分的起始地址

明白这句话之后问题的答案就显而易见了,原本正常继承的delete没有问题,因为基类部分就是在内存上面,析构从上往下没问题,,但是虚继承了之后,虚基类的内容就会跑到下面部分,指针指向下面部分去析构,但是上面却没有析构,这样就会有内存泄漏问题

不过我说的这个问题是在windows下vs会这样,,但是linux下g++这种情况,他会自动的给你偏移到起始位置去free的,所以linux下g++这样没问题

tips:上面普通继承里,vfptr为什么画到Base基类部分呢,因为我写的代码派生类的虚函数是从Base继承来的,如果说Base没有虚函数,派生类有虚函数,那么继承图的vfptr就应该画在Devire部分

系统调用hook

hook是一种技术用来截获和修改应用程序或操作系统发出的系统调用请求。其本质就是劫持函数调用

操作系统的RING0-RING3分层结构

简单说:RING3是用户空间,RING0是内核空间

Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。

Windows只使用RING0和RING3,RING0只给操作系统用,RING3谁都能用。

如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。因为有CPU的特权级别作保护。

以Linux+x86为例 操作系统(内核)的代码运行在最高运行级别RING0上,可以使用特权指令,控制中断、修改页表、访问设备等。 应用程序的代码运行在最低运行级别上RING3上,不能做受控操作。如果要做,例如访问磁盘,写文件的操作,则必须通过系统调用(函数),执行系统调用的时候,CPU的运行级别会从RING3切换到RING0,并跳转到系统调用对应的内核代码位置执行,这样内核就替我们完成了设备访问的操作,完成之后再从RING0返回RING3。整个过程也称作用户态和内核态的切换

如果要想做一个劫持函数:三种实现方法:

  1. 在RING3这种用户空间,我们可以编写自己的动态库,利用LD_PRELOAD优先加载同名的自定义函数 (比如 自定义strcmp来覆盖C标准库的strcmp)
  2. 对于RING0内核空间,找到程序调用对应的汇编命令,修改对应的offset地址 (call 0xc106d75c )让他跳到新的内核函数
  3. 利用利用0x80中断劫持system_call,获取sys_call_table的基地址,修改指定函数offset对应的系统调用了

这块具体代码还不是很懂,涉及汇编和底层。。留坑等以后牛逼了,学习一下–

出处:C++函数 - Linux系统调用Hook )

C++函数 - getopt函数

linux下解析传入命令的,实现命令行参数的解析和处理。

int getopt(int argc,char * const argv[ ],const char * optstring);

参数argc和argv分别代表参数个数和内容,参数 optstring为选项字符串

定义一个optstring字符串,用于指定支持的命令行选项,程序用while调getopt解析输入的命令行,解析到相应的字符,在optstring有支持的,去做对应的处理就行

省略…补坑,暂时没用过

Cpp进阶