前言
这次主要讲类的基础、构造函数与析构函数的使用,以及继承和多态。
类的基本概念
在C++中,类是一种用于创建对象的数据结构,它封装了数据(属性)和操作这些数据的方法(函数)。类定义了对象的特性和行为。
基本结构
一个类通常包括以下部分:
- 数据成员:代表对象的属性。
- 成员函数:定义对象可以执行的操作。
- 访问修饰符:如 public、private 和 protected,用于控制成员的访问权限。
类与结构体的区别
- 在C++中,类和结构体非常相似,但一个主要区别是默认的访问权限。在类中,默认的成员访问权限是私有(private),而在结构体中,默认是公有(public)。
示例代码
下面是一个简单的类定义和使用的例子:
#include <iostream> #include <string> // 定义一个类 class Person { public: // 公有访问修饰符 // 构造函数 Person(std::string name, int age) : name(name), age(age) {} // 成员函数 void introduce() { std::cout << "Name: " << name << ", Age: " << age << std::endl; } private: // 私有访问修饰符 // 数据成员 std::string name; int age; }; int main() { // 创建Person类的对象 Person person("Alice", 30); // 调用成员函数 person.introduce(); return 0; }
在这个例子中:
Person 类有两个私有数据成员:name 和 age。
类有一个公共构造函数,它接受 name 和 age 参数来初始化对象。
introduce 是一个公共成员函数,用于打印对象的信息。
在 main 函数中,我们创建了一个 Person 对象并调用了它的 introduce 方法。
当然,让我们详细探讨C++中类的属性和方法,同时提供一个完整的代码示例。
类的属性和方法
属性(成员变量)
- 属性是类中定义的变量,它们代表对象的状态或特性。在类中声明属性,可以使得每个对象都有自己的一套属性。
方法(成员函数)
- 方法是类中定义的函数,它们用于执行操作,可以访问和修改对象的属性。方法提供了与对象交互的接口。
访问修饰符
- 访问修饰符决定了类成员的访问级别。最常用的有三种:
- public:可以被任何外部代码访问。
- private:只能被类的其他成员(方法或友元)访问。
- protected:只能被类本身、派生类及友元访问。
示例代码
下面是一个类的属性和方法的简单示例:
#include <iostream> #include <string> class Car { public: // 构造函数 Car(std::string model, int year) : model(model), year(year) {} // 公共方法 void displayInfo() { std::cout << "Car Model: " << model << ", Year: " << year << std::endl; } // 设置年份 void setYear(int newYear) { year = newYear; } // 获取年份 int getYear() { return year; } private: // 私有属性 std::string model; int year; }; int main() { // 创建Car类的对象 Car myCar("Toyota", 2020); // 调用公共方法 myCar.displayInfo(); // 更新年份 myCar.setYear(2022); // 再次显示信息 myCar.displayInfo(); return 0; }
在这个例子中:
Car 类有两个私有属性:model 和 year。
公共方法 displayInfo 用于打印汽车信息。
另外两个公共方法 setYear 和 getYear 分别用于设置和获取年份。
在 main 函数中,我们创建了一个 Car 对象,展示了如何使用这些方法来操作对象的状态。
类的构造函数和析构函数
构造函数
- 构造函数在创建类的对象时自动调用。
- 它的主要作用是初始化对象的属性。
- 构造函数的名称与类名相同,并且没有返回类型。
- 可以有参数,也可以没有(默认构造函数)。
- 可以进行重载,即一个类可以有多个构造函数,只要它们的参数列表不同。
析构函数
- 析构函数在对象销毁时自动调用。
- 它的主要作用是进行清理工作,如释放资源、关闭文件等。
- 析构函数的名称是类名前加上波浪符号(~),没有返回类型,也不接受参数。
- 每个类只能有一个析构函数。
示例代码
下面是一个包含构造函数和析构函数的C++类示例:
#include <iostream> #include <string> class Book { public: // 构造函数 Book(std::string title, std::string author) : title(title), author(author) { std::cout << "Book '" << title << "' by " << author << " created." << std::endl; } // 析构函数 ~Book() { std::cout << "Book '" << title << "' by " << author << " destroyed." << std::endl; } // 显示书籍信息的方法 void displayInfo() { std::cout << "Title: " << title << ", Author: " << author << std::endl; } private: std::string title; std::string author; }; int main() { // 创建Book对象 Book myBook("1984", "George Orwell"); // 使用对象的方法 myBook.displayInfo(); // 对象myBook在这里会被自动销毁,调用析构函数 return 0; }
在这个例子中:
Book 类有两个私有属性:title 和 author。
它有一个构造函数,用于初始化这些属性,并打印一条创建消息。
还有一个析构函数,当对象被销毁时,它会打印一条销毁消息。
main 函数中创建了一个 Book 对象,然后调用了它的 displayInfo 方法。程序结束时,myBook 对象的析构函数会被自动调用。
构造函数和析构函数使得对象的初始化和销毁自动执行,有助于管理资源和避免内存泄漏。
类的构造函数重载
在C++中,可以为一个类定义多个构造函数,只要它们的参数列表不同。这就是所谓的构造函数重载。重载构造函数允许以不同的方式初始化同一个类的对象。
重载构造函数
- 目的:提供不同的方式来初始化对象。
- 方法:在同一个类中定义多个构造函数,每个构造函数的参数类型、个数或顺序至少有一个不同。
- 使用场景:需要根据不同的数据或条件创建对象时非常有用。
示例代码
下面的例子演示了如何在C++中重载构造函数:
#include <iostream> #include <string> class Rectangle { public: // 构造函数1:接受两个参数 Rectangle(int w, int h) : width(w), height(h) { std::cout << "Rectangle created with width " << width << " and height " << height << std::endl; } // 构造函数2:接受一个参数 explicit Rectangle(int size) : width(size), height(size) { std::cout << "Square created with size " << size << std::endl; } // 方法:计算面积 int getArea() const { return width * height; } private: int width, height; }; int main() { // 使用第一个构造函数 Rectangle rect1(10, 20); // 使用第二个构造函数 Rectangle rect2(10); std::cout << "Area of rect1: " << rect1.getArea() << std::endl; std::cout << "Area of rect2: " << rect2.getArea() << std::endl; return 0; }
在这个例子中:
Rectangle 类有两个构造函数。第一个接受两个参数(宽和高),第二个仅接受一个参数(正方形的边长)。
两个构造函数具有不同的参数列表,因此它们被重载了。
main 函数中,我们分别使用两种不同的构造函数创建了两个 Rectangle 对象。
通过重载构造函数,Rectangle 类可以灵活地根据提供的参数来初始化对象,使得对象的创建更加灵活。
类中的拷贝构造函数和赋值运算符
拷贝构造函数
- 拷贝构造函数在创建类的一个新对象时使用另一个对象的内容进行初始化时调用。
- 它通常有一个参数,即对同类型对象的引用。
- 如果没有自定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数,进行逐成员的拷贝。
赋值运算符
- 赋值运算符用于将一个对象的内容复制到另一个已经存在的对象中。
- 与拷贝构造函数不同,赋值运算符在对象已经初始化后使用。
- 如果不自定义赋值运算符,编译器将提供一个默认的,进行逐成员的赋值。
示例代码
下面是一个展示拷贝构造函数和赋值运算符使用的示例:
#include <iostream> #include <string> class Person { public: // 构造函数 Person(std::string name) : name(name) {} // 拷贝构造函数 Person(const Person& other) : name(other.name) { std::cout << "Copied Person: " << name << std::endl; } // 赋值运算符 Person& operator=(const Person& other) { if (this != &other) { // 防止自赋值 name = other.name; } return *this; } // 显示姓名 void printName() { std::cout << "Person's name is: " << name << std::endl; } private: std::string name; }; int main() { Person person1("Alice"); Person person2 = person1; // 调用拷贝构造函数 Person person3("Bob"); person3 = person1; // 调用赋值运算符 person1.printName(); person2.printName(); person3.printName(); return 0; }
在这个示例中:
Person 类包含一个构造函数、一个拷贝构造函数和一个赋值运算符。
当我们用 person1 初始化 person2 时,调用了拷贝构造函数。
当我们将 person1 赋值给 person3 时,调用了赋值运算符。
拷贝构造函数和赋值运算符对于管理资源(如动态分配的内存)非常重要,可以防止诸如浅拷贝和资源泄漏等问题。在设计类时,根据需要合理地实现这两个函数是非常重要的。
类中的静态成员
静态成员可以是变量或函数,它们属于类本身,而不是类的任何特定对象。这意味着静态成员由类的所有对象共享。
静态成员变量
- 静态成员变量是类的所有对象共享的变量。它们不属于任何单个对象。
- 静态变量在程序的生命周期内只被初始化一次,并且在类的所有对象之间共享。
- 它们通常用于存储类级别的信息,比如对象计数。
静态成员函数
- 静态成员函数可以在不创建类的对象的情况下调用。
- 它们只能访问静态成员变量和其他静态成员函数。
- 通常用于执行不依赖于对象状态的操作。
示例代码
以下是一个包含静态成员的C++类的示例:
#include <iostream> class MyClass { public: // 构造函数 MyClass() { // 每创建一个对象,计数器增加 objectCount++; } // 静态成员函数 static int getObjectCount() { return objectCount; } private: // 静态成员变量 static int objectCount; }; // 初始化静态成员变量 int MyClass::objectCount = 0; int main() { MyClass obj1; MyClass obj2; MyClass obj3; // 直接通过类名调用静态成员函数 std::cout << "Total objects: " << MyClass::getObjectCount() << std::endl; return 0; }
在这个例子中:
MyClass 类有一个静态成员变量 objectCount,用于跟踪创建的对象数量。
类中有一个静态成员函数 getObjectCount,它返回 objectCount 的值。
在 main 函数中,我们创建了三个 MyClass 的对象,并通过类名直接调用 getObjectCount 来显示创建的对象总数。
类中的继承
继承是面向对象编程的一个基本概念,它允许基于一个类(基类)创建一个新的类(派生类)。这种机制提供了代码重用和层次化分类的能力。
继承的类型
公有继承(public inheritance):基类的公有成员和保护成员在派生类中保持其原有的访问级别。
保护继承(protected inheritance):基类的公有成员和保护成员在派生类中变成保护成员。
私有继承(private inheritance):基类的公有成员和保护成员在派生类中变成私有成员。
示例代码公有继承
#include <iostream> #include <string> // 基类 class Animal { public: Animal(std::string name) : name(name) {} void eat() { std::cout << name << " is eating." << std::endl; } protected: std::string name; }; // 派生类 class Dog : public Animal { public: Dog(std::string name) : Animal(name) {} void bark() { std::cout << name << " is barking." << std::endl; } }; int main() { Dog myDog("Buddy"); myDog.eat(); // 调用基类方法 myDog.bark(); // 调用派生类方法 return 0; }
在这个例子中:
Animal 是一个基类,具有一个公有方法 eat 和一个受保护的成员变量 name。
Dog 是从 Animal 公有继承而来的派生类。它继承了 Animal 的特性,并添加了自己的方法 bark。
在 main 函数中,我们创建了一个 Dog 对象,可以调用从基类继承的 eat 方法以及派生类的 bark 方法。
当然,我可以提供私有继承和保护继承的示例,以进一步阐释这些继承类型在C++中的应用。
示例代码:私有继承
#include <iostream> #include <string> // 基类 class Vehicle { public: Vehicle(std::string type) : type(type) {} protected: std::string type; }; // 派生类 - 私有继承 class Car : private Vehicle { public: Car(std::string type) : Vehicle(type) {} void showType() { std::cout << "Car type: " << type << std::endl; // 可以访问基类的保护成员 } }; int main() { Car myCar("SUV"); myCar.showType(); // 正确 // 下面的代码将会产生编译错误,因为type在Car中是私有的 // std::cout << myCar.type << std::endl; return 0; }
示例代码:保护继承
#include <iostream> #include <string> // 基类 class Device { public: Device(std::string name) : name(name) {} protected: std::string name; }; // 派生类 - 保护继承 class Printer : protected Device { public: Printer(std::string name) : Device(name) {} void printName() { std::cout << "Printer name: " << name << std::endl; // 可以访问基类的保护成员 } }; int main() { Printer myPrinter("HP LaserJet"); myPrinter.printName(); // 正确 // 下面的代码将会产生编译错误,因为name在Printer中是保护的 // std::cout << myPrinter.name << std::endl; return 0; }
多态
多态是面向对象编程的一个核心概念,允许对象以不同的方式响应相同的消息(或方法调用)。在C++中,多态主要通过虚函数(virtual functions)实现。
多态的类型
- 编译时多态:也称为静态多态,主要通过函数重载和运算符重载实现。
- 运行时多态:也称为动态多态,主要通过虚函数和函数覆盖(override)实现。
虚函数和动态多态
- 虚函数允许派生类重写(override)基类中的函数。
- 使用虚函数时,类的行为将根据对象的实际类型,而非其声明类型来确定。
- 动态多态只能通过指针或引用来实现。
示例代码
以下是一个展示C++中使用虚函数实现多态的示例:
#include <iostream> // 基类 class Shape { public: // 虚函数 virtual void draw() const { std::cout << "Drawing a shape." << std::endl; } // 虚析构函数 virtual ~Shape() {} }; // 派生类1 class Circle : public Shape { public: void draw() const override { // 重写虚函数 std::cout << "Drawing a circle." << std::endl; } }; // 派生类2 class Rectangle : public Shape { public: void draw() const override { // 重写虚函数 std::cout << "Drawing a rectangle." << std::endl; } }; void drawShape(const Shape& shape) { shape.draw(); // 动态绑定 } int main() { Circle circle; Rectangle rectangle; drawShape(circle); // 输出: Drawing a circle. drawShape(rectangle); // 输出: Drawing a rectangle. return 0; }
在这个示例中:
Shape 是一个基类,具有一个虚函数 draw 和一个虚析构函数。
Circle 和 Rectangle 是从 Shape 派生的类,它们重写了 draw 方法。
drawShape 函数接受一个 Shape 类型的引用,并调用 draw 方法。由于 draw 是虚函数,实际调用的是对象的动态类型对应的方法,这就是多态的体现。
友元函数和友元类
友元在C++中是一种特殊的机制,允许特定的函数或类访问另一个类的私有(private)和保护(protected)成员。
友元函数
- 友元函数不是类的成员,但它可以访问类的所有私有和保护成员。
- 友元函数通过在类内部使用 friend 关键字声明。
- 友元函数常用于操作两个类的私有数据,例如,重载某些运算符时。
友元类
- 友元类的所有成员函数都可以访问另一个类的私有和保护成员。
- 通过将一个类声明为另一个类的友元,可以提高类间的耦合性。
- 友元关系不是相互的。如果类A是类B的友元,这并不意味着类B是类A的友元。
示例代码
以下是展示友元函数和友元类的示例:
#include <iostream> // 前向声明 class Box; class Contents { public: explicit Contents(int value) : value(value) {} // 友元函数声明 friend void showContents(const Box& b); private: int value; }; class Box { public: explicit Box(int secret) : secret(secret) {} // 友元类声明 friend class SecretInspector; private: int secret; Contents contents{123}; // 假设内容是私有的 }; // 友元类 class SecretInspector { public: void inspect(const Box& b) { std::cout << "Box secret: " << b.secret << std::endl; } }; // 友元函数实现 void showContents(const Box& b) { std::cout << "Box contents: " << b.contents.value << std::endl; } int main() { Box myBox(42); SecretInspector inspector; inspector.inspect(myBox); // 访问Box的私有数据 showContents(myBox); // 访问Box中Contents的私有数据 return 0; }
在这个示例中:
Box 类有一个私有成员 secret 和一个私有嵌套对象 contents。
SecretInspector 类被声明为 Box 的友元类,因此它可以访问 Box 的所有私有成员。
showContents 函数被声明为 Box 的友元函数,因此它可以访问 Box 中的 contents 的私有成员。
运算符重载
运算符重载是一种形式的多态,允许你定义或改变运算符(如 +, -, *, / 等)在自定义类型(如类或结构体)上的行为。
运算符重载的基本概念
- 目的:使自定义类型的对象可以使用标准运算符进行操作。
- 方法:通过在类内定义一个特殊的成员函数或友元函数来实现。这个函数使用 operator 关键字后跟要重载的运算符符号。
- 限制:不能创建新的运算符,只能重载已有的运算符。某些运算符(如 ::, .*, . 和 ?:)不能被重载。
示例代码
下面是一个如何重载加法运算符的示例:
#include <iostream> class Coordinate { public: Coordinate(int x, int y) : x(x), y(y) {} // 运算符 '+' 重载 Coordinate operator+(const Coordinate& other) const { return Coordinate(x + other.x, y + other.y); } // 输出坐标的友元函数 friend std::ostream& operator<<(std::ostream& os, const Coordinate& coord); private: int x, y; }; // 输出运算符 '<<' 的重载 std::ostream& operator<<(std::ostream& os, const Coordinate& coord) { os << "(" << coord.x << ", " << coord.y << ")"; return os; } int main() { Coordinate point1(1, 2); Coordinate point2(3, 4); Coordinate sum = point1 + point2; std::cout << "Sum of coordinates: " << sum << std::endl; return 0; }
在这个示例中:
- Coordinate 类重载了加法运算符(+),使其能够用于两个坐标对象的加法。
- 重载运算符+返回一个新的 Coordinate 对象,其 x 和 y 值是两个操作数对象的 x 和 y 值之和。
- 同时,为了能够方便地打印坐标,重载了输出运算符(<<)。
模板类
模板类是一种强大的C++特性,允许编写与数据类型无关的通用代码。
模板类的基本概念
- 目的:实现代码的泛型编程,即相同的代码可以用于不同的数据类型。
- 方法:使用关键字 template 定义模板,后面跟一个或多个模板参数。
- 使用场景:当你需要类似的行为对多种数据类型执行时,比如容器类(如 vector、map 等)。
示例代码
以下是一个定义和使用模板类的示例:
#include <iostream> // 模板类定义 template<typename T> class Box { public: Box(T content) : content(content) {} T getContent() { return content; } private: T content; }; int main() { // 使用模板类创建整数盒子 Box<int> intBox(123); // 使用模板类创建字符串盒子 Box<std::string> stringBox("Hello World"); std::cout << "Integer Box contains: " << intBox.getContent() << std::endl; std::cout << "String Box contains: " << stringBox.getContent() << std::endl; return 0; }
在这个示例中:
Box 类是一个模板类,它有一个模板参数 T。这个 T 可以在创建 Box 对象时被替换为任何类型。
类中的 content 成员变量和 getContent 方法都使用了模板参数 T。
在 main 函数中,我们创建了两个 Box 对象:一个用于整数 (int),另一个用于字符串 (std::string)。
高级特性
虚继承
- 用途:解决多重继承中的菱形继承问题(即两个派生类继承自同一个基类,再被另一个类同时继承时的冗余和歧义问题)。
- 实现:通过 virtual 关键字在继承时使用。
多重继承
- 用途:允许一个类同时从多个基类继承。
- 注意事项:需要小心处理由多个基类引入的复杂性,如成员冲突和菱形继承问题。
抽象类
- 用途:作为基类,定义一个接口,但不完全实现所有功能,留给派生类去实现。
- 特点:包含至少一个纯虚函数(用 = 0 表示)。
接口
- 概念:在C++中通常通过完全抽象的类实现(即所有成员函数都是纯虚函数)。
- 用途:定义一个类必须遵循的方法,但不提供任何实现。
虚继承示例代码
#include <iostream> class Base { public: virtual void print() { std::cout << "Base" << std::endl; } }; // 虚继承 class Derived1 : virtual public Base { public: void print() override { std::cout << "Derived1" << std::endl; } }; class Derived2 : virtual public Base { public: void print() override { std::cout << "Derived2" << std::endl; } }; class Final : public Derived1, public Derived2 { public: // 调用最近的覆盖 void print() override { Derived2::print(); } }; int main() { Final obj; obj.print(); // 输出 "Derived2" // 通过基类引用访问 Base& baseRef = obj; baseRef.print(); // 同样输出 "Derived2" return 0; }
在这个示例中:
Base 是一个基类,Derived1 和 Derived2 都通过虚继承继承自 Base。
Final 类从 Derived1 和 Derived2 继承,解决了潜在的菱形继承问题。
print 方法在 Derived1 和 Derived2 中被覆盖,并在 Final 中再次覆盖。
当然,我可以为多重继承、抽象类和接口提供相应的C++示例。
多重继承
多重继承允许一个类同时继承自多个基类。
示例代码:多重继承
#include <iostream> // 基类1 class Printer { public: void print() { std::cout << "Printing document" << std::endl; } }; // 基类2 class Scanner { public: void scan() { std::cout << "Scanning document" << std::endl; } }; // 派生类,从两个基类继承 class MultifunctionMachine : public Printer, public Scanner {}; int main() { MultifunctionMachine mfm; mfm.print(); // 调用Printer的成员 mfm.scan(); // 调用Scanner的成员 return 0; }
抽象类
抽象类至少包含一个纯虚函数,并且不能直接实例化。
示例代码:抽象类
#include <iostream> // 抽象基类 class Shape { public: // 纯虚函数 virtual void draw() const = 0; }; // 派生类 class Circle : public Shape { public: void draw() const override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Circle circle; circle.draw(); // 调用派生类的实现 // Shape shape; // 错误:不能实例化抽象类 return 0; }
接口
在C++中,接口可以通过完全抽象的类(只有纯虚函数)实现。
示例代码:接口
#include <iostream> // 接口 class Drawable { public: virtual void draw() const = 0; }; // 实现接口的类 class Rectangle : public Drawable { public: void draw() const override { std::cout << "Drawing a rectangle." << std::endl; } }; int main() { Rectangle rect; rect.draw(); // 调用具体实现 return 0; }
总结
这次从类的基础知识开始,讲了构造函数、析构函数、以及类的成员变量和方法。
接着,讲了重载构造函数、拷贝构造函数、赋值运算符以及静态成员。
然后讲了继承、多态、友元类和友元函数。
最后模板类的概念,还有一些高级特性的基本概念