C++:基础知识

目录

struct和class的区别

final和override关键字

浅拷贝和深拷贝

内联函数和宏定义

 new和delete

malloc与free的实现原理

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?


struct和class的区别

struct class
相同点
  • 两者都拥有成员函数、公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成
不同点 struct默认是public继承 class默认是private继承

final和override关键字

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写(override),以下方法都可以:

class A
{
    virtual void foo();
}
class B : public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

浅拷贝和深拷贝

浅拷贝

        浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

示例如下:

#include   
#include 
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student(){
        name = new char(20);
		cout << "Student" << endl;
    };
	~Student(){
        cout << "~Student " << &name << endl;
        delete name;
        name = NULL;
    };
	Student(const Student &s){//拷贝构造函数
        //浅拷贝,当对象的name和传入对象的name指向相同的地址
        name = s.name;
        //深拷贝
        //name = new char(20);
        //memcpy(name, s.name, strlen(s.name));
        cout << "copy Student" << endl;
    };
};
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}
//浅拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***

//深拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0

内联函数和宏定义

内联函数:是一种特殊的函数,编译器会尝试将其内联展开,而不是通过函数调用的方式执行。内联函数通常用于执行简单的操作或者频繁调用的函数,以减少函数调用的开销和提高性能。

  1. 在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
  2. 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
  3. 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义。
  4. 内联函数有类型检测、语法判断等功能,而宏没有。

内联函数适用场景:

  • 使用宏定义的地方都可以使用 inline 函数。
  • 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。

内联函数的示例:

#include 

// 内联函数示例
inline int addNumbers(int a, int b) {
    return a + b;
}

int main() {
    int result = addNumbers(5, 3);  // 内联函数调用
    std::cout << "Result: " << result << std::endl;

    return 0;
}

 new和delete

new的实现原理

  1. 当使用new运算符分配内存时,编译器会首先计算所需的内存大小,包括对象本身的大小和可能的额外内存(如虚函数表等)。
  2. 编译器会生成相应的代码,调用运行时库中的分配函数(如operator new)来分配所需大小的内存块。
  3. 运行时库会使用底层的内存分配机制(如操作系统提供的malloc()函数)来获取一块连续的内存空间。
  4. 运行时库会将分配到的内存进行适当的对齐,并返回一个指向该内存块的指针。

delete 的实现原理:

  1. 当使用 delete 运算符释放内存时,编译器会生成相应的代码,调用运行时库中的释放函数(如 operator delete)。
  2. 运行时库会接收到要释放的内存指针,并执行必要的清理操作(如调用对象的析构函数)。
  3. 运行时库会使用底层的内存释放机制(如操作系统提供的 free() 函数)来释放内存空间。

malloc与free的实现原理

  1. 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、munmap这些系统调用实现的。
  2. brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
  3. malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。
  4. malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

初始化方式有两种:

1、赋值初始化,通过在函数体内进行赋值初始化;

class MyClass {
private:
    int number;
    std::string name;

public:
    MyClass() {
        number = 0;
        name = "Default";
    }
    
    void printData() {
        std::cout << "Number: " << number << std::endl;
        std::cout << "Name: " << name << std::endl;
    }
};

2、列表初始化,在冒号后使用初始化列表进行初始化。

class MyClass {
private:
    int number;
    std::string name;

public:
    MyClass(int num, const std::string& n) : number(num), name(n) {
        // 构造函数体
    }
    
    void printData() {
        std::cout << "Number: " << number << std::endl;
        std::cout << "Name: " << name << std::endl;
    }
};

这两种方式的主要区别在于:

对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

一个派生类构造函数的执行顺序如下:

① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③ 类类型的成员对象的构造函数(按照成员对象在类中的定义顺序)

④ 派生类自己的构造函数。

为什么用成员初始化列表会快一些?

        赋值初始化是在构造函数当中做赋值的操作,而列表初始化是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

        通过构造函数初始化列表,编译器可以生成更高效的代码,避免了临时对象的创建和额外的赋值操作。这种直接初始化的方式可以提高代码的性能,并且在某些情况下,还可以优化构造函数的调用。

你可能感兴趣的:(C++,c++,开发语言)