阿秀C++笔记-学习记录

81.C++中的组合和继承相比的优缺点

在C中组合一对象系用描述对象包对象系组一个拥对象例其变合类的含的现。这的量类当有员被创建。

以下一个示例,展示了在C++中如何实现组合关系:

class Engine {
    // Engine class definition...
};

class Car {
    Engine engine;
    // Other member variables and functions...
};

int main() {
    Car car;
    // Access car's engine through the member variable:
    car.engine.start(); // Example method call
    
    return 0;
}

在上面的示例中,Car 类中的 engine 成员变量是一个 Engine 类的对象。通过组合,Car 类中的每个对象都拥有一个引擎对象作为其一部分。在主函数中,我们可以通过 car.engine 来访问 Car 对象的引擎,并调用相关方法。

需要注意的是,组合关系不同于继承关系。在组合中,对象之间的关系是 “has-a”,而在继承中,对象之间的关系是 “is-a”。组合关系更多地体现了对象之间的整体与部分的关系。

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

继承

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

85.函数调用过程栈的变化,返回值和参数变量哪个先入栈?

1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;

2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);

3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);

4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;

对于大部分的编程语言和操作系统,被调用函数通常会在进入函数之前保存调用者函数的栈底地址(也称为帧指针,Frame Pointer)和栈顶地址。

下面是一般的函数调用过程:

  1. 当一个函数被调用时,调用者函数的指令执行流会被跳转到被调用函数的入口点。

  2. 在被调用函数中,首先会将调用者函数的栈底地址保存到栈中,这个操作一般由 push ebp 指令完成。

  3. 接下来,将当前栈顶地址(esp)保存到ebp寄存器中,以成为新的栈底地址。这个过程使用 mov ebp, esp 指令完成。

  4. 这样,被调用函数就保存了调用者函数的栈底地址,并且在ebp寄存器中保存了新的栈底地址,可以使用ebp来访问函数参数和局部变量。

  5. 函数执行完成后,会使用 pop ebp 指令将保存的栈底地址还原为调用者函数的栈底地址,以返回到调用者函数。

这种操作可以建立一个新的函数执行帧,使得被调用函数能够正确地访问参数和局部变量,并在函数执行完毕后能够正确返回到调用者函数的执行位置。

需要注意的是,上述过程是一种常见的方式,但并非所有编程语言和操作系统都采用相同的方式来管理函数调用的栈帧。具体的实现可能会有所不同。

在典型的函数调用过程中,被调函数通常会使用栈来存放局部变量和临时变量。栈的延伸方向可以是向下或向上,取决于特定的体系结构和操作系统约定。在大多数情况下,栈的延伸方向是向下的,也就是地址依次减小。

对于采用向下延伸的栈,函数中的局部变量和临时变量会从ebp指针处往下分配内存空间,并按照变量定义的顺序依次排列。具体步骤如下:

  1. 将ebp的值保存到临时寄存器,通常使用mov指令将ebp的值存储到esp或者eax等寄存器。

  2. 将esp的值赋给ebp,将ebp的值设置为当前的栈顶地址,即将ebp指向当前函数的栈帧底部。

  3. 为该函数的局部变量和临时变量分配内存空间。根据变量的类型和大小,在ebp的位置逐渐减小的方向上分配适当的内存空间。

  4. 变量根据定义的顺序依次被分配在栈上,先定义的变量会在栈中较低的位置。

  5. 在函数的执行过程中,可以通过使用ebp加上偏移量来访问局部变量和临时变量。

需要注意的是,这种栈帧的实现方式是一种常见的做法,但并非所有的编程语言和操作系统都采用相同的方式。具体的实现可能会根据编程语言、编译器或操作系统的不同而有所变化。

91、你知道重载运算符吗?

1、 我们只能重载已有的运算符,而无权发明新的运算符;对于一个重载的运算符,其优先级和结合律与内置类型一致才可以;不能改变运算符操作数个数;

2、 两种重载方式:成员运算符和非成员运算符,成员运算符比非成员运算符少一个参数;下标运算符、箭头运算符必须是成员运算符;

3、 引入运算符重载,是为了实现类的多态性;

4、 当重载的运算符是成员函数时,this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数;

5、 从参数的个数推断到底定义的是哪种运算符,当运算符既是一元运算符又是二元运算符(+,-,*,&);

6、 下标运算符必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;

7、 箭头运算符必须是类的成员,解引用通常也是类的成员;重载的箭头运算符必须返回类的指针;

对第2条的解释
C++ 中的运算符重载可以使用两种方式:成员运算符重载和非成员运算符重载。它们在语法上存在一些差异,其中成员运算符重载比非成员运算符重载少一个参数。

  1. 成员运算符重载:使用类的成员函数作为运算符重载函数,通过成员函数来定义运算符的行为。在成员运算符重载中,运算符重载函数将被重载的运算符作为成员函数的一部分,因此成员运算符重载函数只需一个参数。

例如,可以通过重载类的成员函数 operator+() 来实现两个对象的相加运算:

class MyClass {
public:
    int value;

    MyClass() : value(0) {}

    MyClass operator+(const MyClass& other) {
        MyClass result;
        result.value = this->value + other.value;
        return result;
    }
};

MyClass obj1;
obj1.value = 10;
MyClass obj2;
obj2.value = 20;
MyClass sum = obj1 + obj2;  // 使用成员运算符重载
  1. 非成员运算符重载:使用独立于类的函数实现运算符重载。在非成员运算符重载中,重载函数不是类的成员函数,在函数参数中需要显式传递操作数。因此,非成员运算符重载函数需要两个参数(一个或两个操作数)。

例如,在上面的示例中,可以使用非成员函数来实现相加运算符的重载:

class MyClass {
public:
    int value;

    MyClass() : value(0) {}
};

MyClass operator+(const MyClass& obj1, const MyClass& obj2) {
    MyClass result;
    result.value = obj1.value + obj2.value;
    return result;
}

MyClass myObj1;
myObj1.value = 10;
MyClass myObj2;
myObj2.value = 20;
MyClass sum = myObj1 + myObj2;  // 使用非成员运算符重载

注意,在实现非成员运算符重载时,我们没有使用成员函数,而是使用了一个独立的函数,并将对象作为参数进行操作。

无论是成员运算符重载还是非成员运算符重载,都可以实现对运算符的自定义行为,但使用成员运算符重载时,函数只接收一个操作数,而使用非成员运算符重载时,函数需要显式传递操作数。选择哪种方式主要取决于设计需求和语义上的差异。

131、介绍一下几种典型的锁

读写锁

多个读者可以同时进行读
写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

条件变量(Condition Variable)常与互斥锁(Mutex)一起使用,以避免竞态条件(Race Condition)的发生。

竞态条件是指多个线程并发执行时,由于执行顺序不确定而导致结果的正确性受到影响的情况。当多个线程访问共享资源时,如果没有适当的同步机制,就可能引发竞态条件。

互斥锁用于保护共享资源的访问,它可以确保在任意时刻只有一个线程能够访问被保护的资源,从而避免竞态条件的发生。

但有些情况下,仅使用互斥锁可能不足以满足程序的需求。例如,当一个线程等待某个条件达成后再继续执行,就需要使用条件变量。

条件变量允许线程在满足特定条件之前阻塞,并等待其他线程发出的信号。它通常与互斥锁一起使用,以确保线程的等待和唤醒操作在互斥的情况下进行。

在使用条件变量时,通常会遵循以下模式:

  1. 线程获取互斥锁。

  2. 检查条件是否满足,如果不满足,则进入等待状态,释放互斥锁。

  3. 其他线程改变条件并发出信号。

  4. 等待的线程被唤醒后,重新获取互斥锁,并检查条件是否满足。

  5. 如果条件仍未满足,则回到步骤2继续等待。

使用条件变量可以确保线程在等待特定条件时被正确地阻塞,并在条件满足时被正确地唤醒。这样,互斥锁和条件变量的组合可以有效地避免竞态条件的出现。

内存泄漏和内存溢出的场景

下面列举了五种常见的 C++ 内存泄漏和内存溢出的场景:

  1. 忘记释放动态分配的内存:当使用 new 操作符动态分配内存后,如果忘记使用 delete 操作符释放内存,就会导致内存泄漏。例如:
int* ptr = new int;
// 忘记释放内存,导致内存泄漏
  1. 循环引用:当存在两个或多个对象相互引用时,且这些对象使用动态分配的内存,若没有正确地释放这些对象,会导致内存泄漏。例如:
class A {
public:
    B* b;
};

class B {
public:
    A* a;
};

A* obj1 = new A;
B* obj2 = new B;
obj1->b = obj2;
obj2->a = obj1;
// 忘记释放 obj1 和 obj2,导致内存泄漏
  1. 被遗漏的析构函数:当类中定义了资源管理的逻辑(如打开文件、分配内存等),但没有正确实现析构函数来释放这些资源,会导致内存泄漏。例如:
class Resource {
public:
    Resource() {
        // 打开文件或分配内存等资源的初始化操作
    }

    // 没有实现析构函数来释放资源
};

Resource* res = new Resource;
// res 对象没有被正确地释放,导致资源未被释放
  1. 指针迭代器使用错误:当使用指针迭代器(如 STL 容器的迭代器)时,若指针没有正确地增加或减少,可能导致指针访问越界,造成内存溢出。例如:
std::vector<int> vec{1, 2, 3, 4, 5};
auto it = vec.begin();
++it;
++it;
// 忘记检查迭代器是否越界,并导致访问了无效的内存
  1. 递归深度太大:在使用递归函数时,如果递归深度过大,可能导致栈溢出。每次函数调用会将函数的局部变量、返回地址等信息保存在栈上,当递归层级过多时,栈空间可能会被耗尽。例如:
void recursiveFunction() {
    // ...
    recursiveFunction();
}

recursiveFunction();
// 递归层级过多,可能导致栈溢出

这些是可能导致 C++ 内存泄漏和内存溢出的一些常见场景,需要注意在编码中避免这些问题,并及时释放动态分配的内存和资源。使用智能指针、RAII 等技术可以帮助更好地管理内存。

C++内存分配

在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。

  • ,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • ,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 全局/静态存储区,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据(局部static变量,全局static变量)、全局变量和常量。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量字符串,不允许修改。
  • 代码区,存放程序的二进制代码

63.重载实现编译时多态,虚函数实现运行时多态

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针
实现多态有二种方式:覆盖(override),重载(overload)。

覆盖:是指子类重新定义父类的虚函数的做法。

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。

重载实现编译时多态和虚函数实现运行时多态是面向对象编程中常用的两种多态性实现方式。

  1. 重载实现编译时多态:
    重载是指在同一个作用域内,根据不同的参数类型或参数个数定义多个同名函数。编译器在编译时根据函数调用的参数类型来确定调用哪个重载函数,这称为编译时多态。编译时多态可以根据不同参数类型的函数来执行不同的操作,提供了灵活的编程方式。

例如:

void process(int value) {
    // 处理整型参数的函数
}

void process(float value) {
    // 处理浮点型参数的函数
}

int main() {
    int a = 10;
    float b = 2.5f;

    process(a);  // 调用处理整型参数的函数
    process(b);  // 调用处理浮点型参数的函数

    return 0;
}
  1. 虚函数实现运行时多态:

虚函数是在基类中声明为虚函数,并由其派生类重写的函数。通过基类指针或引用调用虚函数时,根据对象实际的动态类型来确定调用哪个重写的函数,这称为运行时多态。运行时多态在运行时根据对象的实际类型,动态地选择调用哪个函数,提供了灵活的面向对象编程方式。

例如:

class Shape {
public:
    virtual void draw() {
        cout << "绘制形状" << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "绘制圆形" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "绘制矩形" << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();

    shape1->draw();  // 调用Circle类的draw函数
    shape2->draw();  // 调用Rectangle类的draw函数

    delete shape1;
    delete shape2;

    return 0;
}

在上述例子中,通过定义基类Shape和派生类Circle和Rectangle,并将派生类重写基类中的虚函数draw。然后通过指向基类的指针shape1和shape2,在运行时根据实际类型调用相应的draw函数,实现了运行时多态性。

64. 成员初始化列表的概念,为什么用它会快一些

成员初始化列表是在类的构造函数中使用初始化列表来初始化类成员变量的一种方式。

使用成员初始化列表可以带来一些性能上的优势,主要有以下几点:

  1. 避免了多余的默认构造和析构过程:在构造函数中,如果类成员变量是通过赋值来初始化的,会先调用默认构造函数创建一个临时对象,然后再调用赋值操作符将临时对象的值赋给成员变量。而使用初始化列表,可以直接在对象创建的过程中将初值传递给成员变量,避免了多余的构造和析构操作,提高了效率。

  2. 对于 const 成员变量和引用类型成员变量,只能通过初始化列表来初始化:const 成员变量在对象创建后就不能被修改,引用类型成员变量必须在创建时绑定到一个对象。使用初始化列表可以直接在对象创建时提供初值,确保 const 成员变量和引用类型成员变量的正确初始化。

  3. 一些复杂的成员对象可能没有默认构造函数或者为其提供默认参数是不可行的:有些类需要非默认的构造函数来接收参数或者执行特定的初始化过程。如果通过赋值操作符来初始化这些成员对象,可能会遇到没有默认构造函数的问题。使用初始化列表可以在构造函数中直接调用相应的构造函数来初始化这些复杂的成员对象。

总的来说,成员初始化列表可以提高初始化的效率,避免多余的操作,并且对于 const 成员变量和引用类型成员变量是必须的。它是一种推荐的初始化方式,特别是在构造函数中有复杂的初始化需求或者对性能要求较高的情况下。

65.C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast

C++中的四种强制转换(static_cast、dynamic_cast、const_cast和reinterpret_cast)有以下区别:

  1. static_cast:主要用于基本类型之间的转换、继承层次结构中的上行转换派生类指针转为基类指针)、void指针和其他指针类型之间的转换等。在编译时完成类型检查,具有一定的安全性。

  2. dynamic_cast:主要用于继承层次结构中的下行转换将基类指针或引用转换为派生类指针或引用)。运行时进行类型检查,如果转换失败会返回空指针(对指针)或抛出std::bad_cast异常(对引用)。

  3. const_cast:主要用于去除指针或引用的const或volatile属性,实现指针或引用的类型转换,用于修改类型的常量性属性。但要注意,const_cast只能用于改变指针或引用的底层const性质,不能用于改变被指对象的常量性。

  4. reinterpret_cast:主要用于指针之间的强制类型转换,它会将一个指针类型转换为完全不同的指针类型,即便是不同类型之间的转换,都可以使用reinterpret_cast。它主要用于特殊的场景,如指针和整数之间的转换,或者不同种类指针之间的转换,但使用时需要谨慎,因为它执行的是低级操作,可能会导致未定义行为。

需要注意的是,任何类型之间的强制转换都可能带来潜在的风险,因此在使用强制转换时需要谨慎,并确保转换的安全性和正确性。

delete和delete[]的区别

deletedelete[]是在C++中用于释放动态分配的内存的操作符,它们有一些区别:

  1. delete用于释放使用new操作符分配的单个对象的内存。例如:MyClass* obj = new MyClass; delete obj;。它会调用对象的析构函数,并释放对应的内存。

  2. delete[]用于释放使用new[]操作符分配的数组对象的内存。例如:int* arr = new int[5]; delete[] arr;。它会调用数组中每个元素的析构函数(如果有的话),然后释放整个数组所占的内存空间。

需要注意的是,使用delete释放使用new[]分配的数组对象的内存(或使用delete[]释放使用new分配的单个对象的内存)是不正确的,这种行为会导致未定义的行为,可能会造成内存泄漏或程序崩溃。

因此,当你使用new分配单个对象时,请使用delete释放内存;当你使用new[]分配数组对象时,请使用delete[]释放内存。这样可以确保正确释放内存并避免潜在的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值