C++ 对象、继承和引用深入解析
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)
来初始化数据成员 x
和 y
。成员初始化列表的效率更高,尤其是对于那些初始化成本较高的成员变量,如自定义类型的成员变量。
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
修饰符介于 public
和 private
之间。protected
成员在类内部和派生类中可以被访问,但在类外部不能被访问。这在继承关系中非常有用,我们将在后续的继承部分详细讨论。
C++ 继承
继承是面向对象编程的重要特性之一,它允许我们基于一个已有的类创建新的类。新类(称为派生类或子类)可以继承基类(或父类)的成员,并且可以添加新的成员或重写基类的成员。
1. 继承的基本语法
继承的语法形式为:
class DerivedClass : accessModifier BaseClass {
// 派生类成员
};
accessModifier
可以是 public
、private
或 protected
,它决定了基类成员在派生类中的访问权限。
例如,假设有一个 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
继承中,基类的public
和protected
成员在派生类中都变成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
类是抽象类,因为它包含纯虚函数 getArea
。Circle
类继承自 Shape
并实现了 getArea
函数,因此 Circle
类可以实例化对象。
C++ 引用
引用是 C++ 中对对象的别名,它提供了一种方便的方式来访问对象,而不需要使用指针。
1. 引用的基本概念与声明
引用的声明形式为 type& referenceName = object;
,其中 type
是被引用对象的类型,referenceName
是引用的名称,object
是被引用的对象。
int num = 10;
int& ref = num;
这里 ref
是 num
的引用,对 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
函数中,a
和 b
是对实参 x
和 y
的引用,通过引用直接修改了实参的值。
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++ 编程的核心基础。