跳至主要內容

面向对象(一)

CodeShouhu大约 28 分钟使用指南Markdown

面向对象(一)

1,简述一下什么是面向对象

面向过程(Procedure-Oriented,简称PO)是一种以过程为中心的编程思想,侧重于描述实现问题的步骤和过程。它将问题分解为一系列函数或方法,每个函数或方法负责完成特定的任务。面向过程的编程风格通常更加直接和简洁,注重代码的效率和性能。

面向对象(Object-Oriented,简称OO)是一种以对象为中心的编程思想,侧重于描述事物的属性和行为。它将问题分解为一系列对象,每个对象代表一个具体的实体或概念,具有自己的状态和行为。面向对象的编程风格通常更加抽象和灵活,注重代码的可重用性、可扩展性和可维护性。

速记

面向过程:侧重于描述实现问题的步骤和过程,根据业务逻辑从上到下写代码

面向对象:侧重于描述事物的属性和行为,将数据与函数绑定到一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程

2,简述一下面向对象的三大特征

C++中面向对象的三大特征是封装、继承和多态。

封装(Encapsulation)是指将客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。封装是面向对象的特征之一,是对象和类概念的主要特性。

继承(Inheritance)是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。

多态(Polymorphism)是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。多态表现为父类引用变量可以指向子类对象,前提条件为必须存在继承关系并且要有方法的重写,在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法

多态的调用格式:父类类型变量名=new子类类型();

总之,封装、继承和多态是C++中面向对象编程的三大核心特征,它们使得程序更加易于理解、扩展和维护。

速记

封装一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。仅对外公开接口来和对象进行 交互。

继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法,并在无需重新编写原来的类的情况下对这些功能进行扩展。

多态是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态。

3,简述一下 C++ 的重载和重写,以及它们的区别

C++中的重载(Overloading)和重写(Overriding)是两个重要的概念,它们都是多态的一部分,但它们在应用上有明显的区别。

**重载(Overloading)**是编译器通过函数参数的类型和数量来决定使用哪个函数的过程。这意味着你可以在同一作用域中定义多个同名函数,只要它们的参数列表(类型或数量)不同。例如:

void func(int);
void func(double);

上述两个函数都有相同的名字,但由于参数类型不同,所以编译器可以根据传入参数的类型来决定调用哪个函数。

**重写(Overriding)**是派生类提供一个与基类同名且参数列表相同的函数的过程。生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内)重写的函数在运行时根据对象的实际类型来调用。重写就是重写函数体,要求基类函数必须是虚函数有virtual修饰。且与基类的虚函数有相同的参数个数 有相同的参数类型 相同的返回值类型

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

  1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
  2. 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
  3. 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
  4. 重写用虚函数来实现,结合动态绑定。
  5. 纯虚函数是虚函数再加上 = 0。
  6. 抽象类是指包括至少一个纯虚函数的类。

纯虚函数:virtual void fun()=0。即抽象类必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。

例如:

class Base {
public:
    virtual void func() { /* ... */ }
};

class Derived : public Base {
public:
    void func() override { /* ... */ }
};

在这个例子中,Derived类重写了Base类的func函数。如果你有一个Base指针指向Derived对象,并调用func函数,那么将会调用Derived类的func函数。

它们的主要区别在于:

  • 重载是编译时多态,根据参数的类型和数量来决定调用哪个函数;重写是运行时多态,根据对象的实际类型来决定调用哪个函数。
  • 重载可以在同一作用域中进行,重写则在基类和派生类之间进行。
  • 重载的函数可以有不同的参数列表,重写的函数必须有相同的参数列表。
  • 重载不一定需要虚函数或者继承,重写则需要虚函数和继承。

速记

封装一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。仅对外公开接口来和对象进行 交互。

继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法,并在无需重新编写原来的类的情况下对这些功能进行扩展。

多态是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态。

4,C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。

是的,C++中的构造函数可以根据其功能和用途分为以下几类:

  1. 默认构造函数:没有任何参数的构造函数称为默认构造函数。如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。

例如:

class MyClass {
public:
    MyClass() { /* 默认构造函数 */ }
};
  1. 初始化构造函数:有时也被称为参数化构造函数,它接受一个或多个参数,用于初始化类的成员变量。

例如:

class MyClass {
private:
    int x;
public:
    MyClass(int val) : x(val) { /* 初始化构造函数 */ }
};
  1. 拷贝构造函数:这种构造函数接受一个同类对象的引用作为参数,用于创建一个新的对象,其内容与传入的对象相同。拷贝构造函数通常在以下情况下被调用:当对象被以值传递的方式传入函数时,当对象从函数中以值返回时,或者当基于另一个同类型的对象显式创建对象时。拷贝初始化首先使用指定构造函数创建一个临时对象, 然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。形参传递是调用的被赋值对象的拷贝构造函数

例如:

class MyClass {
private:
    int x;
public:
    MyClass(const MyClass& other) : x(other.x) { /* 拷贝构造函数 */ }
};
  1. 移动构造函数:C++11引入了移动语义和移动构造函数。移动构造函数接受一个右值引用作为参数,它允许开发者将资源从一个对象移动到另一个对象,而不是进行昂贵的复制操作。这通常用于优化性能。

例如:

class MyClass {
private:
    int* x;
public:
    MyClass(MyClass&& other) : x(other.x) { /* 移动构造函数 */ }
};

这些分类有助于我们理解和使用不同的构造函数,以满足不同的编程需求。

5,c++中只定义析构函数,会自动生成哪些构造函数

在C++中,如果你只定义了一个析构函数,编译器会自动为你生成以下构造函数:

  1. 默认构造函数(无参构造函数):该构造函数不接受任何参数,并初始化对象的所有成员变量。
  2. 拷贝构造函数:该构造函数接受一个同类型的对象作为参数,并创建一个新对象,其成员变量的值与新对象相同。

需要注意的是,如果你已经定义了任何一个构造函数(包括析构函数),编译器就不会自动生成默认构造函数。此外,如果你定义了拷贝构造函数,编译器也不会自动生成相应的构造函数。

6,说说一个类,默认会生成哪些函数

在C++中,如果一个类没有定义任何构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符,编译器会自动为该类生成以下函数:

  1. 默认构造函数(无参构造函数):该构造函数不接受任何参数,并初始化对象的所有成员变量。
  2. 析构函数:该函数在对象被销毁时自动调用,用于释放对象分配的资源。
  3. 拷贝构造函数:该构造函数接受一个同类型的对象作为参数,并创建一个新对象,其成员变量的值与新对象相同。
  4. 移动构造函数:该构造函数接受一个同类型的右值引用对象作为参数,并创建一个新对象,将其成员变量的值从原对象中移动过来。
  5. 拷贝赋值运算符:该运算符接受一个同类型的对象作为参数,并将其成员变量的值赋给当前对象。
  6. 移动赋值运算符:该运算符接受一个同类型的右值引用对象作为参数,并将其成员变量的值移动给当前对象。

以下是一个示例类,展示了编译器自动生成的函数:

#include <iostream>

class MyClass {
public:
    int value;

    // 默认构造函数
    MyClass() {
        std::cout << "Default constructor called." << std::endl;
        value = 0;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        std::cout << "Copy constructor called." << std::endl;
        value = other.value;
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
        value = other.value;
        other.value = 0;
    }

    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        std::cout << "Copy assignment operator called." << std::endl;
        value = other.value;
        return *this;
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "Move assignment operator called." << std::endl;
        value = other.value;
        other.value = 0;
        return *this;
    }
};

在这个示例中,MyClass 类有一个整型成员变量 value。编译器会自动生成默认构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符。在类的定义中,我们手动实现了这些函数,并在函数体内添加了输出语句以观察它们的调用情况。

7,说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序

在 C++ 中,类对象的初始化顺序遵循以下规则:

  1. 首先,基类的静态成员变量和静态成员函数在任何类对象创建之前就已经被初始化和定义。
  2. 然后,基类的非静态成员变量按照声明的顺序进行初始化。
  3. 接着,基类的构造函数被调用,以初始化基类的成员变量。
  4. 如果有多个基类,它们的初始化顺序按照它们在派生类中的声明顺序进行。
  5. 然后,派生类的非静态成员变量按照声明的顺序进行初始化。
  6. 最后,派生类的构造函数被调用,以初始化派生类的成员变量。

在多重继承的情况下,初始化顺序仍然遵循以上规则,但是有一些细节需要注意:

  1. 多个基类按照它们在派生类中的声明顺序进行初始化。
  2. 如果多个基类有共同的基类,那么共同的基类只会被初始化一次。
  3. 如果派生类同时继承自多个基类,而这些基类又有一个共同的基类,那么共同的基类会在第一个被初始化的基类中进行初始化。

因此:1.创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);⒉如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数),4.成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;

父类构造函数->成员类对象构造函数->自身构造函数

其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。析构顺序和构造顺序相反。

下面是一个示例代码,展示了多重继承下类对象的初始化顺序:

#include <iostream>

class Base1 {
public:
    Base1() {
        std::cout << "Base1 constructor called." << std::endl;
    }
};

class Base2 {
public:
    Base2() {
        std::cout << "Base2 constructor called." << std::endl;
    }
};

class CommonBase : public Base1, public Base2 {
public:
    CommonBase() {
        std::cout << "CommonBase constructor called." << std::endl;
    }
};

class Derived : public CommonBase, public Base2 {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
};

int main() {
    Derived obj;
    return 0;
}

在这个示例中,Derived 类多重继承了 CommonBaseBase2 类,而 CommonBase 类又继承了 Base1Base2 类。输出结果为:

Base1 constructor called.
Base2 constructor called.
CommonBase constructor called.
Base2 constructor called.
Derived constructor called.

可以看到,Base1Base2 类在 CommonBase 类之前被初始化,然后 CommonBase 类在其构造函数中调用了 Base1Base2 类的构造函数。接着,Base2 类再次被初始化,最后是 Derived 类的构造函数被调用。

8,简述下c++中向上转型和向下转型

在 C++ 中,向上转型(Upcasting)和向下转型(Downcasting)是多态性中的两种重要操作,它们涉及到基类和派生类之间的转换。

向上转型(Upcasting)是指将派生类的指针或引用转换为基类的指针或引用。这种转换是安全的,因为每个派生类对象都可以被视为基类对象。向上转型可以通过隐式转换自动完成,不需要额外的语法。

例如,假设有一个基类 Shape 和一个派生类 CircleCircle 继承了 Shape。我们可以将 Circle 类型的对象赋值给 Shape 类型的指针或引用,这就是向上转型:

Shape* shape;
Circle circle;
shape = &circle; // 向上转型,将 Circle 类型的指针转换为 Shape 类型的指针

向下转型(Downcasting)是指将基类的指针或引用转换为派生类的指针或引用。这种转换是有风险的,因为不是每个基类对象都可以被视为派生类对象。向下转型需要使用动态类型识别(Dynamic Type Identification,简称 DTI)来检查转换的有效性。在 C++ 中,可以使用 dynamic_cast 运算符进行向下转型。

例如,继续上面的例子,我们可以尝试将 Shape 类型的指针转换为 Circle 类型的指针:

Shape* shape;
Circle circle;
shape = &circle; // 向上转型

Circle* circlePtr = dynamic_cast<Circle*>(shape); // 向下转型
if (circlePtr != nullptr) {
    // 转换成功,circlePtr 指向 Circle 类型的对象
} else {
    // 转换失败,shape 不是指向 Circle 类型的对象
}

在使用向下转型时,需要注意以下几点:

  1. 必须使用指针或引用进行向下转型,因为只有在运行时才能确定转换是否有效。
  2. 必须使用 dynamic_cast 运算符进行向下转型,它会检查转换的有效性并返回空指针(nullptr)或有效的派生类指针。
  3. 在进行向下转型之前,最好使用向上转型将指针或引用转换为基类类型,以确保转换的安全性。

9,简述下深拷贝和浅拷贝,如何实现深拷贝

深拷贝和浅拷贝是编程中常见的概念,它们涉及到对象或数据的复制方式。

浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

例如,在C++中,如果使用赋值运算符(=)将一个对象赋值给另一个对象,或者使用拷贝构造函数创建一个新对象,默认情况下会进行浅拷贝。假设有一个包含指针成员的类,浅拷贝只会复制指针的值,而不会复制指针指向的数据。

深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

要实现深拷贝,需要在自定义的类中提供自定义的拷贝构造函数和赋值运算符重载函数。在拷贝构造函数和赋值运算符重载函数中,需要手动分配新的资源,并将原始对象的数据复制到新资源中。这样可以确保新对象和原始对象完全独立。

以下是一个示例代码,展示了如何实现深拷贝:

#include <iostream>
#include <cstring>

class MyString {
public:
    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    ~MyString() {
        delete[] data;
    }

    // 自定义拷贝构造函数,实现深拷贝
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
    }

    // 自定义赋值运算符重载函数,实现深拷贝
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            delete[] data;
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

private:
    char* data;
    int length;
};

在上面的示例中,MyString 类有一个指针成员 data,用于存储字符串数据。在自定义的拷贝构造函数和赋值运算符重载函数中,首先为新对象分配了新的资源,然后使用 strcpy 函数将原始对象的数据复制到新资源中。这样可以确保新对象和原始对象完全独立,修改其中一个对象不会影响到另一个对象。

10,简述一下 C++ 中的多态

在 C++ 中,多态是指同一操作作用于不同的对象,可以产生不同的结果。C++ 支持两种类型的多态:静态多态和动态多态。

静态多态(Static Polymorphism)是指在编译时就确定函数调用的方式,也称为早绑定(Early Binding)。静态多态是通过函数重载和模板实现的。

函数重载是指在同一个作用域内可以定义多个同名函数,但是它们的参数列表(参数类型、参数个数或参数顺序)必须不同。编译器在编译时会根据函数调用时提供的实参类型和个数选择合适的函数进行调用。

例如,下面是一个简单的函数重载的例子:

#include <iostream>

void print(int i) {
    std::cout << "Printing int: " << i << std::endl;
}

void print(double d) {
    std::cout << "Printing double: " << d << std::endl;
}

int main() {
    print(5);      // 调用 print(int)
    print(3.14);   // 调用 print(double)
    return 0;
}

模板是一种实现泛型编程的工具,它允许程序员定义一种适用于多种数据类型的函数或类。编译器在编译时会根据模板参数的实际类型生成相应的函数或类。

例如,下面是一个简单的模板函数的例子:

#include <iostream>

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add<int>(2, 3) << std::endl;      // 输出 5
    std::cout << add<double>(2.5, 3.5) << std::endl; // 输出 6
    return 0;
}

动态多态(Dynamic Polymorphism)是指在运行时才确定函数调用的方式,也称为晚绑定(Late Binding)。动态多态是通过虚函数和继承实现的。

在基类中声明一个虚函数,然后在派生类中重写这个虚函数,就可以实现动态多态。在运行时,通过基类的指针或引用调用虚函数时,会根据实际对象的类型来确定调用哪个函数。

例如,下面是一个简单的动态多态的例子:

#include <iostream>

class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing Shape..." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing Circle..." << std::endl;
    }
};

int main() {
    Shape* shape = new Circle;
    shape->draw();  // 输出 "Drawing Circle..."
    delete shape;
    return 0;
}

12,简述一下 C++ 中的多态的底层原理

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调 用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

这里需要引出虚表和虚基表指针的概念。

虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表

虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针

发现虚函数,自动生成虚表存放入口地址 派生类定义对象创建虚表初始化父类子类

(1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是虚表里保存了虚函数的入口地址,在派生类定义对象时,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表

(2)编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数

(4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。

虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

13,说说类继承时,派生类对不同关键字修饰的基类方法的访问权限

类中的成员可以分为三种类型,分别为public成员、protected成员、public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。

  1. public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。
  2. protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象不可以访问基类的public、protected、private成员。
  3. private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象不可以访问基类的public、protected、private成员。

14,移动构造函数与其使用案例

移动构造函数是 C++11 引入的一种特殊类型的构造函数,用于在对象移动时以最小代价的方式构造对象。移动构造函数通常会将其他对象的资源移动到新创建的对象中,而不是在新对象中创建新的资源。这样可以避免不必要的资源分配和释放操作,提高程序的性能。

移动构造函数的定义如下:

class_name(class_name&& other);

其中,class_name 是类的名称,&& 表示右值引用,other 是一个同类型的右值引用参数。

以下是一个简单的案例,展示了如何定义和使用移动构造函数:

#include <iostream>
#include <vector>

class MyString {
public:
    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    ~MyString() {
        delete[] data;
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept {
        length = other.length;
        data = other.data;
        other.length = 0;
        other.data = nullptr;
    }

private:
    char* data;
    int length;
};

int main() {
    std::vector<MyString> vec;
    for (int i = 0; i < 100000; i++) {
        vec.push_back(MyString("hello")); // 使用移动构造函数优化性能
    }
    return 0;
}

在上面的案例中,我们定义了一个简单的 MyString 类,并在其中实现了移动构造函数。在 main 函数中,我们创建了一个包含 10 万个 MyString 对象的 vector 容器。由于我们使用了移动构造函数,所以在将对象添加到 vector 容器时,会避免不必要的资源分配和释放操作,从而提高程序的性能。

15,请你回答一下 C++ 类内可以定义引用数据成员吗?

c++类内可以定义引用成员变量,但要遵循以下三个规则:

  1. 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误。
  2. 构造函数的形参也必须是引用类型。
  3. 不能在构造函数里初始化,必须在初始化列表中进行初始化。

是的,C++ 类内可以定义引用数据成员。引用数据成员是一个类的成员变量,它是一个对其他对象的引用。通过在类内定义引用数据成员,可以将类的实例与另一个对象关联起来,使得这两个对象可以共享相同的资源。

下面是一个简单的例子,展示了如何在 C++ 类内定义引用数据成员:

#include <iostream>

class MyClass {
public:
    MyClass(int& ref) : ref_member(ref) {}
    
    void print() const {
        std::cout << "Reference member value: " << ref_member << std::endl;
    }
    
private:
    int& ref_member; // 引用数据成员
};

int main() {
    int x = 42;
    MyClass obj(x);
    obj.print(); // 输出:Reference member value: 42
    x = 100;
    obj.print(); // 输出:Reference member value: 100
    return 0;
}

在这个例子中,MyClass 类有一个私有的引用数据成员 ref_member,它是一个对整数的引用。在 MyClass 的构造函数中,通过传递一个整数的引用给 ref_member 来初始化它。这样,MyClass 的实例就能够访问和修改 ref_member 所引用的整数的值。在 main 函数中,我们创建了一个 MyClass 的实例 obj,并将整数 x 的引用传递给 ref_member。随后,我们修改了 x 的值,并调用了 obj.print() 来验证 ref_member 是否引用了正确的值。

16,构造函数为什么不能被声明为虚函数?

构造函数不能被声明为虚函数的原因主要有以下几点:

  1. 虚函数的主要目的是实现动态多态,允许在派生类中重写基类中的函数。但是在调用构造函数时,对象还没有完全创建,所以虚表(包含虚函数指针的表)还没有被设定,这意味着虚函数机制在这个时候无法工作。
  2. 构造函数是被用来初始化类的新实例的。每个类都有自己的构造函数,当我们创建类的实例时,该类的构造函数就会被调用。因此,构造函数是不能被继承的,也就不能被重写,所以不能声明为虚函数。
  3. 虚函数表是在构造函数运行时创建的,而构造函数本身是不能被继承的。如果构造函数是虚函数,那么就需要通过虚函数表来调用,但是此时虚函数表还没有被创建,这将导致无法调用构造函数。

所以,基于以上原因,构造函数不能被声明为虚函数。

17,简述一下什么是常函数,有什么作用

常函数是指一类特殊的函数,它们在被调用时不会修改任何数据成员,也就是说,它们只是读取数据成员的值,但不会对其进行修改。常函数在C++中是通过在函数声明和定义时添加const关键字来定义的。

常函数的主要作用是增加程序的安全性和健壮性。它们可以确保在函数被调用时,不会意外地修改数据成员的值,从而避免了因误操作而导致程序出错或数据不一致的问题。此外,常函数还可以提高代码的可读性和可维护性,因为它清晰地表明了函数的功能和目的。

需要注意的是,常函数不能被声明为虚函数,因为它不能被重写。此外,在常函数中也不能调用非常函数,因为非常函数可能会修改成员变量,这与常函数的限制是相悖的。

以下是一个使用常函数的案例:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : m_value(value) {}
    
    int getValue() const { // 常函数
        return m_value;
    }
    
private:
    int m_value;
};

int main() {
    const MyClass obj(42); // 创建常对象
    std::cout << obj.getValue() << std::endl; // 调用常函数,输出 42
    // obj.m_value = 100; // 错误!无法修改常对象的数据成员
    return 0;
}

在这个案例中,我们定义了一个名为MyClass的类,它有一个私有数据成员m_value和一个公有常函数getValue()。在main函数中,我们创建了一个MyClass的常对象obj,并调用了它的getValue()函数。由于getValue()是一个常函数,它不会修改obj的任何数据成员,从而保证了程序的安全性。