MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 对象、继承和引用深入解析

2023-10-154.3k 阅读

C++ 对象基础

在 C++ 中,对象是类的实例化。类定义了一种数据结构,它包含数据成员(也称为成员变量)和成员函数。当我们根据类创建一个对象时,实际上是在内存中为该对象分配了空间,用于存储其数据成员。

1. 对象的创建与初始化

对象的创建可以通过声明变量的方式进行。例如,假设有一个简单的 Point 类:

class Point {
public:
    int x;
    int y;
};

我们可以这样创建 Point 对象:

Point p1;
p1.x = 10;
p1.y = 20;

这种方式先创建对象,然后再分别赋值给数据成员。但更好的方式是在对象创建时进行初始化,这可以通过构造函数来实现。构造函数是与类名相同的特殊成员函数,用于初始化对象的数据成员。

class Point {
public:
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
};

现在我们可以这样创建并初始化 Point 对象:

Point p2(30, 40);

这里使用了成员初始化列表 : x(a), y(b) 来初始化数据成员 xy。成员初始化列表的效率更高,尤其是对于那些初始化成本较高的成员变量,如自定义类型的成员变量。

2. 对象的生命周期

对象的生命周期取决于其创建的位置和方式。

局部对象:在函数内部声明的对象是局部对象。当函数执行到对象声明处时,对象被创建;当函数执行结束,局部对象的生命周期结束,其占用的内存被释放。

void func() {
    Point p(1, 2);
    // 函数结束时,p 的生命周期结束
}

全局对象:在所有函数外部声明的对象是全局对象。全局对象在程序启动时创建,在程序结束时销毁。

Point globalP(5, 6);
int main() {
    // 程序启动时,globalP 已创建
    return 0;
    // 程序结束时,globalP 被销毁
}

动态分配的对象:通过 new 关键字创建的对象是动态分配的对象,它们存储在堆上。需要使用 delete 关键字来释放其占用的内存,否则会导致内存泄漏。

Point* dynamicP = new Point(7, 8);
// 使用 dynamicP
delete dynamicP;

类的访问控制

C++ 提供了访问修饰符来控制类成员的访问权限,这有助于实现数据封装,即隐藏对象的内部细节,只暴露必要的接口给外部使用。

1. public 修饰符

声明为 public 的成员可以在类的外部被访问。例如:

class Rectangle {
public:
    int width;
    int height;
    int getArea() {
        return width * height;
    }
};

在类外部可以这样访问 public 成员:

Rectangle rect;
rect.width = 10;
rect.height = 20;
int area = rect.getArea();

2. private 修饰符

声明为 private 的成员只能在类的内部被访问,外部代码无法直接访问。这可以保护类的内部数据不被随意修改。

class Circle {
private:
    double radius;
public:
    double getArea() {
        return 3.14159 * radius * radius;
    }
    void setRadius(double r) {
        if (r > 0) {
            radius = r;
        }
    }
};

在类外部,不能直接访问 radius

Circle cir;
// cir.radius = 5; // 错误,radius 是 private 成员
cir.setRadius(5);
double area = cir.getArea();

3. protected 修饰符

protected 修饰符介于 publicprivate 之间。protected 成员在类内部和派生类中可以被访问,但在类外部不能被访问。这在继承关系中非常有用,我们将在后续的继承部分详细讨论。

C++ 继承

继承是面向对象编程的重要特性之一,它允许我们基于一个已有的类创建新的类。新类(称为派生类或子类)可以继承基类(或父类)的成员,并且可以添加新的成员或重写基类的成员。

1. 继承的基本语法

继承的语法形式为:

class DerivedClass : accessModifier BaseClass {
    // 派生类成员
};

accessModifier 可以是 publicprivateprotected,它决定了基类成员在派生类中的访问权限。

例如,假设有一个 Shape 基类:

class Shape {
public:
    virtual double getArea() {
        return 0;
    }
};

我们可以创建一个 Rectangle 派生类:

class Rectangle : public Shape {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    double getArea() override {
        return width * height;
    }
};

这里 Rectangle 类通过 public 继承自 Shape 类,它继承了 Shape 类的 getArea 函数,并根据自身需求重写了该函数。override 关键字用于明确标识派生类中重写的虚函数,这有助于编译器检查是否真的重写了基类的虚函数。

2. 继承中的访问权限

  • public 继承:在 public 继承中,基类的 public 成员在派生类中仍然是 public 的,基类的 protected 成员在派生类中仍然是 protected 的,基类的 private 成员在派生类中不可访问。
  • private 继承:在 private 继承中,基类的 publicprotected 成员在派生类中都变成 private 的,基类的 private 成员在派生类中不可访问。
  • protected 继承:在 protected 继承中,基类的 public 成员在派生类中变成 protected 的,基类的 protected 成员在派生类中仍然是 protected 的,基类的 private 成员在派生类中不可访问。

3. 多态性与虚函数

多态性是指一个接口,多种实现。在 C++ 中,通过虚函数和指针或引用实现运行时多态。

虚函数:在基类中声明为 virtual 的函数称为虚函数。派生类可以重写这些虚函数以提供自己的实现。

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal speaks" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog barks" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat meows" << std::endl;
    }
};

当我们使用基类指针或引用来调用虚函数时,实际调用的是对象真正类型对应的函数版本,这就是运行时多态。

void makeSound(Animal& animal) {
    animal.speak();
}

int main() {
    Dog dog;
    Cat cat;
    makeSound(dog); // 输出 "Dog barks"
    makeSound(cat); // 输出 "Cat meows"
    return 0;
}

在这个例子中,makeSound 函数接受一个 Animal 引用,无论传入的是 Dog 对象还是 Cat 对象,都会调用其对应的 speak 函数版本。

4. 纯虚函数与抽象类

纯虚函数:是在基类中声明但没有实现的虚函数,其语法形式为 virtual returnType functionName(parameters) = 0;。包含纯虚函数的类称为抽象类,抽象类不能实例化对象。

class Shape {
public:
    virtual double getArea() = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

这里 Shape 类是抽象类,因为它包含纯虚函数 getAreaCircle 类继承自 Shape 并实现了 getArea 函数,因此 Circle 类可以实例化对象。

C++ 引用

引用是 C++ 中对对象的别名,它提供了一种方便的方式来访问对象,而不需要使用指针。

1. 引用的基本概念与声明

引用的声明形式为 type& referenceName = object;,其中 type 是被引用对象的类型,referenceName 是引用的名称,object 是被引用的对象。

int num = 10;
int& ref = num;

这里 refnum 的引用,对 ref 的操作实际上就是对 num 的操作。

ref = 20;
std::cout << num << std::endl; // 输出 20

2. 引用作为函数参数

引用在函数参数中广泛应用,它可以避免对象的拷贝,提高效率,同时可以实现对实参的修改。

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5;
    int y = 10;
    swap(x, y);
    std::cout << "x: " << x << ", y: " << y << std::endl; // 输出 x: 10, y: 5
    return 0;
}

swap 函数中,ab 是对实参 xy 的引用,通过引用直接修改了实参的值。

3. 引用作为函数返回值

函数也可以返回引用,这在某些情况下很有用,例如实现类似运算符重载的功能。

class Fraction {
private:
    int numerator;
    int denominator;
public:
    Fraction(int num, int den) : numerator(num), denominator(den) {}
    int& getNumerator() {
        return numerator;
    }
};

int main() {
    Fraction frac(3, 5);
    int& num = frac.getNumerator();
    num = 7;
    std::cout << "Numerator: " << frac.getNumerator() << std::endl; // 输出 Numerator: 7
    return 0;
}

这里 getNumerator 函数返回 numerator 的引用,使得外部代码可以直接修改 numerator 的值。

4. 常量引用

常量引用是指向常量对象的引用,声明形式为 const type& referenceName = object;。常量引用可以绑定到临时对象,并且不能通过常量引用修改对象的值。

void printValue(const int& value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    printValue(10); // 可以接受临时对象
    int num = 20;
    const int& ref = num;
    // ref = 30; // 错误,不能通过常量引用修改值
    return 0;
}

5. 引用与指针的区别

  • 语法:引用声明时需要初始化,并且一旦初始化后不能再引用其他对象;指针声明后可以在任何时候指向不同的对象,并且可以不初始化。
int num1 = 10;
int& ref = num1;
// ref = num2; // 错误,不能重新绑定引用

int num2 = 20;
int* ptr = &num1;
ptr = &num2;
  • 内存占用:引用本身不占用额外的内存空间(在大多数实现中),它只是对象的别名;指针需要占用一定的内存空间来存储对象的地址。
  • 空值:引用不能为 nullptr,因为引用必须始终引用一个有效的对象;指针可以为 nullptr,表示不指向任何对象。

对象、继承和引用的综合应用

在实际编程中,对象、继承和引用经常结合使用。

1. 继承体系中的对象创建与初始化

当创建派生类对象时,首先会调用基类的构造函数来初始化基类部分,然后调用派生类的构造函数来初始化派生类特有的部分。

class Base {
public:
    int baseValue;
    Base(int value) : baseValue(value) {
        std::cout << "Base constructor" << std::endl;
    }
};

class Derived : public Base {
public:
    int derivedValue;
    Derived(int base, int derived) : Base(base), derivedValue(derived) {
        std::cout << "Derived constructor" << std::endl;
    }
};
int main() {
    Derived obj(10, 20);
    return 0;
}

在这个例子中,创建 Derived 对象 obj 时,首先调用 Base 类的构造函数初始化 baseValue,然后调用 Derived 类的构造函数初始化 derivedValue

2. 多态性与引用

在多态的场景下,引用经常与继承一起使用。例如,通过基类引用调用派生类重写的虚函数。

class Shape {
public:
    virtual double getArea() {
        return 0;
    }
};

class Rectangle : public Shape {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    double getArea() override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};
void printArea(Shape& shape) {
    std::cout << "Area: " << shape.getArea() << std::endl;
}
int main() {
    Rectangle rect(5, 10);
    Circle cir(3);
    printArea(rect);
    printArea(cir);
    return 0;
}

这里 printArea 函数接受一个 Shape 引用,通过该引用可以调用不同派生类对象的 getArea 函数,实现多态性。

3. 引用在继承体系中的传递

在继承体系中,引用可以在不同层次的类之间传递,并且可以保持多态性。

class A {
public:
    virtual void show() {
        std::cout << "A::show" << std::endl;
    }
};

class B : public A {
public:
    void show() override {
        std::cout << "B::show" << std::endl;
    }
};

class C : public B {
public:
    void show() override {
        std::cout << "C::show" << std::endl;
    }
};
void func(A& a) {
    a.show();
}
int main() {
    C c;
    func(c);
    return 0;
}

在这个例子中,func 函数接受 A 引用,当传入 C 对象时,会调用 C 类重写的 show 函数,体现了多态性和引用在继承体系中的传递。

通过深入理解 C++ 的对象、继承和引用,开发者可以编写出更加高效、灵活和可维护的代码,充分发挥 C++ 面向对象编程的强大功能。无论是开发大型软件系统,还是实现复杂的算法,这些概念都是 C++ 编程的核心基础。