考试题型:
- 选择题:20题,共50分(每题2.5分)
- 程序分析与简答题:4大题,共35分(每大题包含若干小题)
- 编程题:1题,15分(关于基类与派生类,要求会写类的构造函数)
复习知识点:
1. 面向对象基本概念
类(Class)
- 类是现实世界中某些具有共同属性和行为的事物的抽象。它定义了一组对象的共同特征和行为。
对象(Object)
- 对象是类的一个具体实例,具有类定义的属性和行为。每个对象都是类的具体化,拥有自己的状态和行为。
封装(Encapsulation)
- 封装是将对象的实现细节隐藏起来,只暴露出一个可以被外界访问的接口。封装可以保护数据,防止外部直接访问和修改内部状态。
继承(Inheritance)
- 继承是一种机制,允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和行为。继承支持代码复用,并可以建立类之间的层次关系。
多态(Polymorphism)
- 多态是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。多态性可以通过虚函数和重载实现。
核心概念解析:
- 类:可以看作是创建对象的蓝图或模板,它定义了对象的结构和行为。
- 对象:是根据类的定义创建的实体,每个对象都拥有类中定义的属性和可以执行类中定义的行为。
- 封装:通过将数据(属性)和行为(方法)组合在一起,并利用访问修饰符(如private、public)来控制对类的访问。
- 继承:子类继承父类的属性和方法,可以扩展或修改父类的行为,支持扩展性和维护性。
- 多态:允许接口有多种不同的数据类型,使得同一个接口可以使用不同的数据类型,增加了程序的灵活性。
2. 值传递形参与引用传递形参
值传递(Pass by Value)
- 定义:在函数调用时,实际上是将实参的值复制一份传递给函数的形参。这意味着形参得到的是实参值的一个副本。
- 特点:
- 形参的变化不会影响实参。
- 传递过程中可能会有性能开销,特别是当传递大型对象时。
- 适用于不需要改变实参值的场景。
引用传递(Pass by Reference)
- 定义:通过引用传递,函数接收的是实参的内存地址,即形参是实参的一个别名。
- 特点:
- 形参对数据的修改会直接影响到实参。
- 避免了复制过程,节省了内存和时间,特别是对于大型对象。
- 可以用来改变实参的值,也可以用来传递大型对象或数组。
区别与应用
- 控制:值传递提供了对数据的控制,防止函数内部对外部数据的修改;引用传递则允许函数修改传入的数据。
- 效率:引用传递通常比值传递更高效,尤其是在处理大型数据结构时。
- 使用场景:当需要在函数内部修改变量时,应使用引用传递;如果只是想保护原始数据不被修改,则使用值传递。
引用传递的应用示例
void modifyByValue(int n) {
n += 10; // 修改n的值,但不影响实参
}
void modifyByReference(int &n) {
n += 10; // 修改n的值,实参也会受到影响
}
int main() {
int a = 5;
modifyByValue(a); // a的值不变
modifyByReference(a); // a的值变为15
return 0;
}
3. 带默认形参值的函数
默认参数值(Default Arguments)
- 定义:在C++中,可以为函数的参数设置默认值。当函数被调用时,如果没有为这些参数提供值,编译器会自动使用默认值。
语法
return_type function_name(parameter1 = default_value1, parameter2 = default_value2, ...) {
// 函数体
}
特点
- 可选参数:函数调用时可以省略一些或全部参数,省略的参数将使用默认值。
- 位置灵活性:在调用函数时,只有从右向左提供实参,左侧的参数将使用默认值。
- 限制:一旦函数的某个参数列表中存在默认值,它之后的所有参数也必须有默认值。
示例
#include <iostream>
using namespace std;
// 带默认参数值的函数
void printInfo(int a, int b = 10, int c = 20) {
cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}
int main() {
printInfo(5); // a = 5, b = 10, c = 20
printInfo(5, 15); // a = 5, b = 15, c = 20
printInfo(5, 15, 25); // a = 5, b = 15, c = 25
return 0;
}
注意事项
- 覆盖默认值:在调用函数时,可以为任意参数位置的参数提供值,未提供值的参数将使用默认值。
- 调用顺序:在提供实参时,应从函数定义的最后一个参数开始向左提供,以确保正确覆盖默认值。
- 默认值作用域:默认参数值只在声明它们的函数中有效,不会影响到重载函数。
应用场景
- 当函数需要接受多个参数,但很多情况下只使用其中一部分时,可以为不常用的参数设置默认值。
- 提供接口的灵活性,允许调用者根据需要选择传递的参数数量。
带默认形参值的函数是C++中提高代码复用性和灵活性的一种方式,使得函数接口更加友好和方便。
4. 函数重载
函数重载概念
- 定义:函数重载是C++中允许多个函数具有相同名称,但参数列表必须不同的特性。这使得同一个操作可以用于不同的数据类型或参数个数。
语法
return_type function_name(parameter_type1, parameter_type2, ...) {
// 函数体
}
return_type function_name(other_parameter_type1, other_parameter_type2, ...) {
// 另一个函数体
}
- 函数名相同,但参数类型、数量或顺序至少有一项不同。
特点
- 参数差异:重载的函数必须在参数列表上有区别。
- 返回类型:返回类型的不同不作为重载的依据。
- 访问权限:重载函数可以有不同的访问权限,如
public
、private
等。 - 常量性:成员函数的重载可以区分普通函数和
const
成员函数。
示例
#include <iostream>
using namespace std;
// 函数重载示例
void print(int a) {
cout << "Integer: " << a << endl;
}
void print(double a) {
cout << "Double: " << a << endl;
}
void print(const char* a) {
cout << "String: " << a << endl;
}
int main() {
print(5); // 调用 int 版本的 print
print(3.14); // 调用 double 版本的 print
print("Hello"); // 调用 const char* 版本的 print
return 0;
}
注意事项
- 参数类型匹配:调用函数时,编译器会根据实参的类型匹配最合适的重载函数。
- 参数个数:如果重载函数的参数个数不同,编译器会根据实参的个数选择函数。
- 最佳匹配:如果有多个重载函数都可以接受给定的参数,编译器会尝试找到最佳匹配项。
应用场景
- 当需要对不同类型的数据执行相同的操作时,可以定义重载函数。
- 提供了一种方法,使得函数名可以表达操作的意图,而不仅仅是操作的数据类型。
函数重载是C++中实现多态性的一种形式,它允许程序员定义具有相同名称但操作不同类型数据的函数,增加了代码的可读性和灵活性。
5. 类的定义
类的概念
- 定义:类是面向对象编程中的基本构建块,它允许将数据和处理这些数据的函数组合在一起。
- 组成:类由属性(成员变量)和方法(成员函数)组成。
类的基本定义方法
class 类名 {
public:
// 公有成员,可以被外部访问
数据类型 成员变量;
void 方法();
private:
// 私有成员,只能在类内部访问
数据类型 私有变量;
};
类成员的访问权限
- 公有(public):类成员可以被外部访问。
- 私有(private):类成员只能在类的内部访问,保护了数据的封装性。
- 保护(protected):类成员在类内部和继承的子类中都可以访问。
类的组成部分
- 数据成员:定义了类的状态,即对象的属性。
- 成员函数:定义了类的行为,即可以对对象执行的操作。
成员函数的定义
- 在类体内定义的成员函数可以省略函数体,称为内联函数。
- 在类体外定义的成员函数需要使用作用域运算符(::)明确其属于哪个类。
示例
class Clock {
public:
// 公有数据成员
int hour;
int minute;
int second;
// 公有成员函数
void setTime(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
// 私有数据成员
private:
int timeZone;
// 私有成员函数(通常用于数据验证等内部逻辑)
bool isValidTime(int h, int m, int s) {
return (h >= 0 && h < 24 && m >= 0 && m < 60 && s >= 0 && s < 60);
}
};
注意事项
- 封装性:通过访问权限控制,保护数据不被外部直接修改,而是通过公共接口进行操作。
- 构造函数:类可以定义构造函数,用于创建对象时初始化数据成员。
- 析构函数:类可以定义析构函数,用于销毁对象时执行清理工作。
类的定义是面向对象编程的核心概念之一,它允许程序员创建具有特定属性和行为的对象。通过合理地使用访问权限,可以保证数据的安全性和类的易用性。
6. 对象的定义与使用
对象的概念
- 定义:对象是类的实例,它是具有类定义的属性和行为的实体。
- 创建:通过类定义,可以创建多个对象,每个对象都有自己的状态和行为。
创建对象
- 语法:使用类名和关键字
new
(对于动态分配)或直接声明来创建对象。 - 示例:
class 类名 { // 类定义 }; int main() { 类名 对象名; // 在栈上创建对象 类名 *指针名 = new 类名; // 动态分配对象 return 0; }
使用对象
- 访问成员:通过对象名和点(
.
)运算符访问对象的成员变量和成员函数。 - 示例:
Clock myClock; // 创建Clock类的对象 myClock.hour = 12; // 访问并设置数据成员 myClock.showTime(); // 调用成员函数
对象数组
- 定义:可以创建对象的数组,数组中的每个元素都是一个对象。
- 示例:
Clock clocks[5]; // 创建包含5个Clock对象的数组 for (int i = 0; i < 5; ++i) { clocks[i].setHour(i * 2); // 设置每个对象的小时 }
对象指针
- 定义:指针变量可以指向一个对象,通过指针可以访问对象的成员。
- 示例:
Clock *clockPtr = &myClock; // 指向对象的指针 clockPtr->hour = 3; // 通过指针访问成员
对象的深复制与浅复制
- 浅复制:复制对象时,只复制对象的成员变量的值,如果成员是指针,只复制指针的值。
- 深复制:复制对象时,不仅复制成员变量的值,还复制指针指向的内存内容。
对象的动态内存分配
- 定义:使用
new
操作符在堆上为对象分配内存。 - 释放:使用
delete
操作符释放动态分配的对象。
注意事项
- 初始化:对象在使用前应该被正确初始化。
- 构造函数:使用构造函数初始化对象的成员变量。
- 析构函数:使用析构函数清理对象使用资源,特别是动态分配的内存。
对象是面向对象编程中的核心概念,它将数据和行为封装在一起,提供了一种直观的方式来模拟现实世界中的实体。通过创建和使用对象,可以实现程序的模块化和数据的封装。
7. 构造函数与复制构造函数
构造函数
- 定义:构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。
- 特点:
- 与类名相同。
- 没有返回类型,包括
void
。 - 可以有参数,用于初始化对象的成员变量。
- 可以重载,以提供不同的初始化方式。
构造函数的作用
- 初始化:为对象的成员变量提供初始值。
- 执行时机:对象创建时自动调用。
构造函数的使用方法
class 类名 {
public:
// 构造函数声明
类名(参数列表) {
// 初始化代码
}
};
int main() {
类名 对象名(实参列表); // 创建对象并调用构造函数
return 0;
}
复制构造函数
- 定义:复制构造函数是一种特殊的构造函数,用于使用一个已存在的对象来初始化新创建的对象。
- 特点:
- 其参数是一个引用,指向同一类的另一个对象。
- 通常用于对象的赋值操作或作为参数传递给函数。
复制构造函数的语法
class 类名 {
public:
// 复制构造函数声明
类名(const 类名& 引用);
};
类名::类名(const 类名& 其他) {
// 使用其他对象的状态来初始化当前对象
}
复制构造函数的使用场景
- 对象赋值:当需要将一个对象的状态复制给另一个对象时。
- 函数参数传递:当函数需要接收一个对象作为参数,并且希望在函数内部修改这个对象时。
- 函数返回:当函数需要返回一个对象时。
示例
class Clock {
public:
Clock(int h, int m, int s) : hour(h), minute(m), second(s) {}
// 复制构造函数
Clock(const Clock& other) : hour(other.hour), minute(other.minute), second(other.second) {}
// 成员变量
int hour, minute, second;
};
int main() {
Clock clock1(12, 0, 0); // 使用普通构造函数创建对象
Clock clock2 = clock1; // 使用复制构造函数创建对象
return 0;
}
注意事项
- 深复制与浅复制:复制构造函数应该确保正确地复制对象的所有成员,包括指针成员指向的数据。
- 默认复制构造函数:如果未显式定义复制构造函数,编译器会生成一个默认的复制构造函数。
构造函数和复制构造函数是类设计中的重要组成部分,它们确保了对象在创建和复制时具有正确的初始状态。正确地实现和使用这些构造函数对于保证程序的正确性和健壮性至关重要。
8. 析构函数
析构函数的概念
- 定义:析构函数是一种特殊的成员函数,它在对象生命周期结束时被调用,用于执行清理工作。
- 特点:
- 与类名相同,但在前面加上了
~
符号。 - 没有返回类型,不接受任何参数。
- 与类名相同,但在前面加上了
析构函数的作用
- 资源释放:释放对象在生命周期中分配的资源,如内存、文件句柄、网络连接等。
- 清理操作:执行其他必要的清理工作,确保程序的健壮性。
析构函数的使用场景
- 对象生命周期结束:自动对象在离开作用域时,动态分配的对象在使用
delete
操作符时。 - 程序结束:当程序运行结束时,所有未被释放的资源将由操作系统回收,但显式调用析构函数可以更优雅地处理资源释放。
析构函数的使用方法
class 类名 {
public:
// 析构函数声明
~类名() {
// 清理代码
}
};
int main() {
类名 对象名; // 对象生命周期结束时,调用析构函数
return 0;
}
示例
class Resource {
public:
Resource() {
// 假设分配了一些资源
}
~Resource() {
// 释放资源
}
};
int main() {
Resource r; // Resource的构造函数被调用
return 0; // Resource的析构函数被调用
}
注意事项
- 自动调用:析构函数在对象生命周期结束时自动调用,无需程序员显式调用。
- 清理顺序:与构造函数的调用顺序相反,先调用成员对象的析构函数,然后是局部对象的析构函数。
- 继承:在派生类中,如果定义了析构函数,应当先调用基类的析构函数,以确保正确释放资源。
析构函数是面向对象编程中管理资源的重要工具,它确保了即使在发生异常时,对象占用的资源也能被正确释放。正确实现析构函数对于防止内存泄漏和其他资源管理问题至关重要。
9. 类的聚合
类聚合的概念
- 定义:类聚合是指一个类作为另一个类的成员变量,即一个对象包含其他对象作为其数据成员。
- 组成:这种关系通常表示为“has-a”关系,即一个对象拥有另一个对象。
类聚合的特点
- 数据共享:通过聚合,可以在不同的类之间共享数据。
- 行为扩展:聚合允许一个类扩展或使用另一个类的行为。
- 独立生命周期:聚合的成员对象拥有自己的生命周期,独立于包含它的对象。
类聚合的使用场景
- 组合对象:当一个对象需要包含或使用另一个对象的功能时。
- 减少代码重复:通过聚合复用已有的类,避免代码重复。
类聚合的示例
class Engine {
public:
void start() {
// 启动引擎的代码
}
};
class Car {
public:
Car() : engine() {} // 在构造Car时,初始化引擎成员
void startEngine() {
engine.start(); // 使用聚合的Engine对象的行为
}
private:
Engine engine; // Car聚合了Engine对象
};
注意事项
- 初始化:聚合的成员对象需要正确初始化,可能需要在构造函数的初始化列表中进行。
- 访问控制:聚合的成员对象的访问权限应根据需要进行设置,以保持封装性。
- 清理责任:通常,包含聚合对象的类不需要负责其成员对象的销毁,除非存在特殊的资源管理需求。
类聚合是面向对象设计中的一种常见模式,它允许灵活地组合不同的功能,同时保持各个类的独立性和可重用性。通过聚合,可以在不同的上下文中重用已有的对象,同时保持代码的清晰和组织性。
10. 构造函数的初始化列表
初始化列表的概念
- 定义:初始化列表是C++中一种特殊的语法,允许在构造函数体内外部直接初始化对象的成员变量。
- 语法:
class 类名 { public: 类名(参数列表) : 初始化列表 { // 构造函数体 } };
初始化列表的作用
- 直接初始化:对于成员变量,特别是常量或引用类型的成员,初始化列表提供了一种直接初始化的方式。
- 效率:使用初始化列表可以提高构造函数的效率,因为它避免了复制或赋值操作。
初始化列表的使用方法
class Person {
public:
Person(const std::string& name, int age)
: name_(name), age_(age) { // 使用初始化列表直接初始化成员变量
}
private:
std::string name_;
int age_;
};
初始化列表的特点
- 顺序:初始化列表中成员变量的初始化顺序应与它们在类声明中的顺序一致。
- 必要性:对于常量成员或引用成员,必须使用初始化列表进行初始化。
- 限制:初始化列表不能包含任何计算或逻辑判断。
示例
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {} // 初始化列表
private:
double x_;
double y_;
};
注意事项
- 构造顺序:初始化列表中的成员变量应按照它们在类声明中出现的顺序进行初始化。
- 成员函数:初始化列表不能包含对非静态成员函数的调用。
- 默认构造函数:如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数,它使用默认的初始化方式。
构造函数的初始化列表是C++中一种高效且必要的初始化方式,特别是对于需要直接初始化的成员变量。它不仅提高了代码的可读性,还有助于保持成员变量的有效状态。
11. 前向引用声明
前向引用声明的概念
- 定义:前向引用声明是一种在C++中使用的声明方式,用于告知编译器某个类或类型的存在,而不需要立即提供类或类型的完整定义。
- 目的:主要用于解决头文件中的循环依赖问题,允许在类的定义之前引用该类。
前向引用声明的语法
class 前向引用的类名;
前向引用声明的作用
- 解决循环依赖:当两个或多个类互相包含对方作为成员,或者互相作为函数参数和返回类型时,可以使用前向声明来避免编译错误。
- 延迟定义:允许在某些情况下推迟类的完整定义,直到真正需要使用该类的所有成员时。
前向引用声明的使用场景
- 类之间的相互引用:当类A中有一个成员变量或函数返回类型是类B,而类B中也包含类A的引用或指针时。
- 模板编程:在模板类或模板函数中,可能需要引用尚未定义的类。
前向引用声明的示例
// 假设有两个类,存在循环依赖
class A; // 前向声明类A
class B {
public:
A* memberA; // B类中包含对A的指针
};
class A { // 定义类A
public:
void functionB(B b); // A类中的函数接受B类的对象作为参数
};
注意事项
- 限制性:前向声明只声明了类的存在,没有提供类的任何具体信息,因此不能用于定义对象、调用成员函数或访问数据成员。
- 使用限制:在使用前向声明的类时,只能进行指针或引用的操作,不能创建对象实例。
- 头文件保护:前向声明通常与头文件保护宏一起使用,以防止头文件被多次包含。
前向引用声明是C++中管理复杂类依赖关系的重要工具,它有助于简化头文件的包含关系,避免编译错误,并提高代码的模块化。
12. 作用域与可见性
作用域的概念
- 定义:作用域(Scope)是程序中一个区域,在这个区域内,声明的变量或对象是可访问的。
- 分类:
- 全局作用域:在所有函数外部声明的变量,整个程序范围内可见。
- 局部作用域:在函数或代码块内部声明的变量,仅在该函数或代码块内部可见。
可见性的概念
- 定义:可见性(Visibility)指在程序的某个位置,变量或对象能否被引用或访问。
作用域规则
- 局部作用域:在代码块内部声明的变量仅在该代码块内部有效。
- 嵌套作用域:内层作用域可以访问外层作用域的变量,但外层作用域不能访问内层作用域的变量。
- 同名变量:内层作用域中的变量会隐藏外层作用域中的同名变量。
可见性的规则
- 声明顺序:在同一个作用域内,变量必须先声明后使用。
- 同名标识符:在不同作用域中声明的同名标识符是不同的,内层作用域的标识符会屏蔽外层作用域的同名标识符。
示例
int globalVar = 10; // 全局作用域
void function() {
int localVar = 5; // 局部作用域
{
int localVar = 7; // 嵌套的局部作用域,隐藏了外层的localVar
// ...
}
// 使用localVar会报错,因为这里的localVar指的是外层的localVar
}
注意事项
- 作用域限定:在C++中,可以使用作用域运算符
::
来明确指定变量的作用域。 - 对象的可见性:对象的成员变量和成员函数的可见性由对象的作用域决定。
- 命名空间:命名空间可以创建不同的作用域,避免名称冲突。
命名空间的作用域
- 定义:命名空间提供了一种将程序中的实体(变量、函数、类等)组织在一起的方法,避免了全局命名冲突。
- 使用:
namespace MyNamespace { int localVar = 20; } int main() { int localVar = 10; // 局部变量,与MyNamespace::localVar不同 MyNamespace::localVar; // 明确指定命名空间的作用域 }
作用域和可见性是C++中重要的概念,它们帮助程序员控制变量和对象的生命周期,以及在不同部分的程序中如何访问它们。正确地理解和使用作用域和可见性规则对于编写清晰、可维护的代码至关重要。
13. 变量/对象的生存期
生存期的概念
- 定义:生存期(Lifetime)是指变量或对象在程序运行期间从创建到销毁的时间范围。
静态生存期(Static Lifetime)
- 定义:具有静态生存期的变量或对象在程序的整个运行期间一直存在,直到程序结束。
- 特点:
- 在任何时刻,它们的状态都是可见的。
- 通常在全局作用域或命名空间作用域中声明。
- 如果在局部作用域声明,需要使用
static
关键字。
动态生存期(Dynamic Lifetime)
- 定义:具有动态生存期的变量或对象只在创建它们的代码块执行期间存在。
- 特点:
- 当代码块执行结束时,它们会被销毁。
- 通常在局部作用域中声明,没有使用
static
关键字。
示例
int globalVar; // 全局变量,静态生存期
void function() {
static int staticVar; // 局部静态变量,静态生存期
int localVar; // 局部变量,动态生存期
// ...
}
注意事项
- 全局变量和静态成员变量:它们在整个程序运行期间都存在,并且在程序的任何地方(如果可见)都可以访问。
- 局部变量:只在函数或代码块内部存在,当函数调用结束或代码块执行完毕后,它们会被销毁。
- 静态变量:即使在局部作用域中,使用
static
关键字声明的变量也会保持它们的值,直到程序结束。
对象的生存期
- 定义:对象的生存期从它们被创建开始,直到它们被销毁。
- 特点:
- 对象的生存期可以是自动的(栈上),动态的(堆上),或由程序员显式管理。
- 对象的成员变量的生存期与对象本身一致。
动态内存分配
- 定义:使用
new
操作符在堆上为对象分配内存,这会延长对象的生存期,直到使用delete
操作符释放内存。 - 示例:
int* dynamicArray = new int[10]; // 动态分配数组 delete[] dynamicArray; // 释放分配的数组
理解静态生存期和动态生存期的概念对于管理程序中资源的生命周期至关重要。正确地处理变量和对象的生存期可以避免内存泄漏和其他资源管理错误。
14. 静态成员
静态成员的概念
- 定义:静态成员属于类而不是类的某个特定对象。它们在所有对象之间共享,并且在整个程序运行期间保持其状态。
静态成员的特点
- 共享性:静态成员由类的所有对象共享,修改静态成员的值会影响所有对象。
- 类作用域:静态成员在类的作用域内,通过类名直接访问,不需要实例化对象。
- 初始化时机:静态成员在程序启动时初始化,并在程序结束时销毁。
静态成员的使用方法
- 访问方式:可以通过
类名::静态成员名
的方式访问。 - 声明与定义:在类内部声明静态成员,通常在类外部定义并初始化。
示例
class Counter {
public:
static int count; // 静态成员变量声明
Counter() { // 构造函数
count++; // 每次创建对象时,计数增加
}
};
int Counter::count = 0; // 静态成员变量定义和初始化
int main() {
Counter obj1;
Counter obj2;
std::cout << "Total objects created: " << Counter::count << std::endl;
return 0;
}
注意事项
- 静态成员函数:可以访问静态成员,但不能访问非静态成员,因为它们与具体对象的状态无关。
- 非静态成员函数:可以访问静态和非静态成员。
- 静态成员的限制:静态成员不依赖于任何对象实例,因此它们不能直接访问非静态成员。
静态成员的应用场景
- 计数器:跟踪创建的类对象的数量。
- 全局常量:作为类相关的常量,所有对象共享。
- 资源管理:管理所有对象共享的资源,如连接池或内存池。
静态成员函数
- 定义:静态成员函数是与类相关联的函数,不依赖于类的任何特定对象。
- 特点:可以通过类名直接调用,不能修改对象的状态。
示例
class MathUtils {
public:
static int factorial(int n) { // 静态成员函数
if (n <= 1) return 1;
return n * factorial(n - 1);
}
};
int main() {
std::cout << "Factorial of 5: " << MathUtils::factorial(5) << std::endl;
return 0;
}
静态成员是面向对象编程中实现数据和行为共享的重要机制。它们为类提供了一种存储和管理所有对象共享数据的方式,同时保持了数据的封装性和一致性。
15. 友元函数
友元函数的概念
- 定义:友元函数是可以直接访问类中的私有(private)和保护(protected)成员的非成员函数。
- 目的:允许特定的外部函数访问类的封装数据,而不必通过类的公共接口。
友元函数的特点
- 非成员:友元函数不是类的成员函数,因此它们在类定义之外定义。
- 访问权限:尽管不是成员函数,但友元函数可以访问类的私有和保护成员。
- 声明方式:在类定义中使用
friend
关键字声明友元函数。
友元函数的作用
- 数据访问:允许外部函数访问类的内部数据,特别是当这些数据需要被类外部的特定函数处理时。
- 操作类数据:实现对类数据的操作,而这些操作不属于类的方法。
友元函数的声明和使用
class MyClass {
private:
int privateData;
public:
void publicMethod() {
// ...
}
// 声明友元函数
friend int getPrivateData(MyClass& obj);
};
// 定义友元函数
int getPrivateData(MyClass& obj) {
return obj.privateData; // 直接访问私有成员
}
注意事项
- 封装原则:尽管友元函数可以访问私有成员,但它们破坏了类的封装性,因此应谨慎使用。
- 作用域:友元函数的定义在类定义之外,需要在类声明中明确指定。
- 继承:友元关系不会被继承,即如果一个类是另一个类的友元,这不会使其成为任何派生类的友元。
友元函数与友元类
- 友元类:一个类可以声明为另一个类的友元类,这意味着该类的所有成员函数都是另一个类的友元函数。
- 声明方式:
class FriendClass; // 前向声明 class MyClass { public: void publicMethod() { // ... } // 声明友元类 friend class FriendClass; };
友元函数是一种强大的机制,可以在不破坏封装性的前提下,允许外部函数访问类的私有数据。然而,由于它们对封装原则的影响,应当在确保必要性和合理性的情况下使用。
16. 常成员函数
常成员函数的概念
- 定义:常成员函数(
const
成员函数)是被声明为const
的成员函数,意味着这些函数保证不会修改类的任何成员变量。
常成员函数的特点
- 不变性:在
const
成员函数中,所有成员变量都是const
,因此不能修改它们。 - 调用限制:常对象只能调用常成员函数。
- 声明方式:在成员函数的声明和定义的末尾添加
const
关键字。
常成员函数的声明和使用
class MyClass {
public:
int getData() const { // 声明常成员函数
return data;
}
// ...
private:
int data;
};
void function(const MyClass& myObj) {
int value = myObj.getData(); // 常对象调用常成员函数
}
注意事项
- 赋值限制:在
const
成员函数内,不能给任何非静态成员变量赋新值。 - 修改限制:不能调用任何非
const
成员函数。 - 构造函数和析构函数:构造函数和析构函数不能被声明为
const
。
常成员函数的应用
- 获取数据:用于获取对象数据而不修改对象状态的函数。
- 与常对象交互:在处理常对象时,需要保证函数不会修改对象状态。
示例
class Circle {
public:
double getRadius() const { // 常成员函数,返回半径
return radius;
}
void setRadius(double r) { // 非const成员函数,设置半径
radius = r;
}
private:
double radius;
};
int main() {
const Circle c(1.0); // 创建常对象
std::cout << c.getRadius(); // 正确:调用常成员函数
// c.setRadius(2.0); // 错误:不能调用非const成员函数
return 0;
}
常成员函数是C++中实现对象不可变性的重要机制,它们确保了在函数执行过程中对象的状态不会被改变。这在多线程环境中尤其重要,因为const
成员函数可以被安全地在不同的线程中调用,而不必担心数据竞争问题。
17. 常数据成员
常数据成员的概念
- 定义:常数据成员(
const
数据成员)是类中被声明为const
的成员变量,意味着这些变量的值一旦初始化后就不能被修改。
常数据成员的特点
- 不可变性:常数据成员的值在对象的整个生命周期内不能被改变。
- 初始化要求:必须在构造函数的初始化列表中进行初始化,不能在构造函数体内进行赋值。
常数据成员的初始化方法
- 构造函数初始化列表:在类中定义的所有常数据成员都必须通过构造函数的初始化列表来初始化。
示例
class MyClass {
public:
MyClass(int val) : constDataMember(val) { // 使用初始化列表初始化常数据成员
}
private:
const int constDataMember; // 常数据成员
};
int main() {
MyClass myObj(10); // 创建对象时初始化constDataMember
// myObj.constDataMember = 20; // 错误:不能修改常数据成员
return 0;
}
注意事项
- 只读性质:常数据成员在整个对象的生存期内都是只读的,任何尝试修改它的操作都会导致编译错误。
- 构造函数中使用:由于常数据成员不能在构造函数体中被赋值,它们必须在所有构造函数的初始化列表中明确初始化。
- 静态常数据成员:静态常数据成员是一个例外,它们可以在类定义外部直接初始化。
静态常数据成员
- 定义:静态常数据成员是类的一个特殊成员,它与类的所有对象共享,并且具有静态生存期。
- 初始化:静态常数据成员可以在类定义外部使用
const
关键字初始化,如果类型允许,也可以在类定义内部直接赋值。
示例
class MyClass {
public:
static const int staticConstDataMember; // 静态常数据成员声明
};
const int MyClass::staticConstDataMember = 10; // 定义和初始化
int main() {
// MyClass::staticConstDataMember = 20; // 错误:不能修改静态常数据成员
std::cout << MyClass::staticConstDataMember << std::endl;
return 0;
}
常数据成员是C++中实现数据不可变性的一种方式,它们在确保数据安全和一致性方面非常有用。然而,它们需要在构造函数的初始化列表中正确初始化,这是使用常数据成员时需要特别注意的一点。
18. 常引用
常引用的概念
- 定义:常引用(
const
引用)是一种特殊的引用,它绑定到一个对象或变量上,并且不能通过这个引用来修改原对象或变量的值。
常引用的特点
- 不可变性:常引用一旦被初始化后,就不能被用来修改它所绑定的对象或变量。
- 用作函数参数:常引用经常用作函数参数,以传递对象或变量的引用,同时保证函数内部不会修改实参的值。
常引用的使用方法
- 声明:使用
const
关键字声明引用类型,指明它是一个常引用。 - 初始化:常引用在声明时必须立即初始化。
示例
void display(const int& num) {
// num 是一个常整型引用,不能通过num修改传入的参数值
std::cout << num << std::endl;
}
int main() {
int value = 10;
display(value); // 正确:传递value的引用给display函数
// display(value + 10); // 错误:常引用必须绑定到一个左值
return 0;
}
注意事项
- 左值和右值:常引用只能绑定到左值上,即必须是一个持久的对象或变量,不能是临时对象或字面量。
- 指针和引用:常引用与指针不同,它本身没有地址的概念,它就是所引用对象的一个别名。
- 与指针的区别:常引用不能被重新赋值指向另一个对象,也不能通过它修改所引用对象的值。
常引用与指针的区别
- 重新赋值:指针可以被重新赋值指向不同的对象,常引用不能。
- 修改能力:指针可以修改其所指向对象的值(除非是
const
指针),常引用则不能修改。
常引用的应用场景
- 函数参数:用于保护传递给函数的对象不被修改。
- 返回值优化:通过引用传递大型对象或数组,避免复制的开销。
常引用是C++中一种有用的机制,它提供了一种安全的方式来传递对象,同时保护这些对象不被函数内部修改。这在编写需要接收对象但不需要修改对象的函数时尤其有用。
19. 对象指针
对象指针的概念
- 定义:对象指针是一种指向类对象或其派生类的指针。它存储了对象在内存中的地址。
对象指针的使用方法
- 声明:使用类名和
*
指针符号声明对象指针。 - 初始化:对象指针可以初始化为指向一个具体对象的地址,或被初始化为
nullptr
。
对象指针的声明
class MyClass {
// 类定义
};
int main() {
MyClass obj; // 创建对象
MyClass *ptr = &obj; // 创建对象指针并初始化为对象的地址
return 0;
}
访问对象成员
- 通过指针访问成员:使用
->
运算符来访问对象的成员。 - 直接访问:如果指针是通过
&
取得地址的,也可以通过解引用操作符*
来访问。
示例
MyClass *ptr = new MyClass(); // 分配动态对象并初始化
ptr->memberFunction(); // 通过指针调用成员函数
(*ptr).memberFunction(); // 通过解引用调用成员函数
delete ptr; // 释放动态分配的对象
注意事项
- 空指针:在声明指针后,如果未初始化或不再需要使用指针指向的对象,应将其设置为
nullptr
。 - 内存管理:使用
new
操作符动态分配对象时,必须使用delete
操作符释放内存,避免内存泄漏。 - 解引用:在通过指针访问对象成员之前,确保指针不是
nullptr
,避免空指针解引用。
对象指针与this指针
- this指针:在类的非静态成员函数中,
this
是一个隐含的指针,指向调用成员函数的对象。
示例
class MyClass {
public:
void showAddress() {
std::cout << "Object address: " << this << std::endl;
}
};
int main() {
MyClass obj;
obj.showAddress(); // 显示对象的内存地址
return 0;
}
对象指针是C++中处理动态对象和实现面向对象编程的重要概念。它们允许程序员通过指针操作对象,实现诸如动态内存分配、对象的多态行为等功能。正确地管理对象指针对于防止内存泄漏和其他资源管理错误至关重要。
20. this
指针
this
指针的概念
- 定义:
this
指针是C++中类的非静态成员函数内部的一个隐含指针,指向调用成员函数的对象本身。
this
指针的作用
- 指向对象:在成员函数中,
this
指针指向调用该成员函数的对象。 - 访问成员:
this
指针用于区分成员变量和局部变量,以及调用类的其他成员函数。
this
指针的使用场景
- 区分同名变量:当成员函数内的局部变量与成员变量同名时,使用
this
指针来区分。 - 成员函数中:在成员函数内部,当需要显式访问对象的成员时。
示例
class MyClass {
int value;
public:
MyClass(int val) : value(val) {}
void printValue() {
std::cout << "Value: " << this->value << std::endl;
// 或者使用 (*this).value,但 this->value 更为常见
}
};
注意事项
- 非静态成员:
this
指针只在类的非静态成员函数中有效。 - 常量成员函数:在常量成员函数中,
this
指针是一个指向常对象的指针。 - 指针常量:
this
指针本身是一个指针常量,不能被修改指向其他对象。
this
指针的用途
- 明确成员访问:在成员函数中,即使没有歧义,也可以使用
this
指针来明确指出正在访问的是成员变量或成员函数。 - 通过对象指针调用:当通过对象指针调用成员函数时,
this
指针由编译器自动解析。
示例
class MyClass {
int data;
public:
void setData(int d) {
data = d; // 直接赋值
this->data = d; // 使用this指针
}
MyClass* getThis() {
return this; // 返回对象本身的地址
}
};
int main() {
MyClass obj;
obj.setData(10);
MyClass* ptr = obj.getThis(); // 获取对象的地址
return 0;
}
this
指针是C++中类成员函数的一个重要特性,它提供了一种方式来访问和操作当前对象的状态。正确使用this
指针有助于编写清晰和易于维护的代码。
21. 指向成员的指针
指向成员的指针的概念
- 定义:指向成员的指针是一种特殊的指针,用于指向类或结构体的成员变量或成员函数。
指向成员的指针的分类
- 指向数据成员的指针:用于指向类中的一个数据成员。
- 指向成员函数的指针:用于指向类中的一个成员函数。
指向成员的指针的声明
- 使用
类名::*
语法声明指向数据成员的指针。 - 使用
类名(::*)(参数列表)
语法声明指向成员函数的指针。
示例
class MyClass {
public:
int data;
void function() {
std::cout << "Function called" << std::endl;
}
};
int main() {
MyClass obj;
MyClass::* ptrToData = &MyClass::data; // 指向数据成员的指针
(void)obj.*ptrToData = 10; // 使用指针访问并赋值成员变量
void (MyClass::*ptrToFunction)() = &MyClass::function; // 指向成员函数的指针
(obj.*ptrToFunction)(); // 使用指针调用成员函数
return 0;
}
注意事项
- 初始化:在声明指向成员的指针后,必须对其进行初始化,使其指向特定的成员。
- 使用:使用
对象名.*指针名
访问数据成员,或(对象名.*指针名)(参数列表)
调用成员函数。 - 作用域:指向成员的指针必须在类或结构的作用域内声明。
指向成员的指针的应用
- 动态访问:在运行时动态地选择访问哪个成员变量或调用哪个成员函数。
- 实现多态:通过指针调用成员函数,利用虚函数机制实现多态行为。
指向静态成员的指针
- 静态成员不属于任何具体对象,因此指向静态成员的指针与普通指针类似。
示例
class MyClass {
public:
static int staticData;
static void staticFunction() {
std::cout << "Static function called" << std::endl;
}
};
int MyClass::staticData = 0; // 定义和初始化静态成员
int main() {
int* ptrToStaticData = &MyClass::staticData; // 指向静态数据成员的指针
*ptrToStaticData = 5; // 使用指针访问并赋值静态成员变量
void (*ptrToStaticFunction)() = &MyClass::staticFunction; // 指向静态成员函数的指针
(*ptrToStaticFunction)(); // 使用指针调用静态成员函数
return 0;
}
指向成员的指针是C++中一种高级特性,它允许程序员在运行时通过指针操作访问类的成员。这在实现某些设计模式或需要高度灵活性的场合非常有用。然而,由于其复杂性,应当谨慎使用,以避免代码难以理解和维护。
22. 动态内存分配
动态内存分配的概念
- 定义:动态内存分配是指在程序运行时(而不是编译时)请求内存,并在不再需要时释放内存的过程。
- 目的:用于创建生存期不局限于创建它们的块作用域的对象。
使用 new
进行内存分配
- 语法:
new 类型名
或new 类型名(初始化参数)
- 功能:为对象分配内存,并可选地使用给定的参数列表调用构造函数来初始化对象。
- 返回:返回一个指向分配对象的指针。
示例
int* ptr = new int; // 分配一个整型对象的内存,并初始化为0
int* arrPtr = new int[10]; // 分配一个整型数组的内存
MyClass* objPtr = new MyClass(10); // 分配MyClass对象的内存并初始化
使用 delete
进行内存释放
- 语法:
delete 指针名
或delete[] 数组指针名
- 功能:释放之前使用
new
分配的内存,并调用对象的析构函数。 - 注意事项:
- 仅释放
new
分配的内存。 delete
对于单个对象,delete[]
对于数组。
- 仅释放
示例
delete ptr; // 释放单个对象的内存
delete[] arrPtr; // 释放数组的内存
注意事项
- 内存泄漏:如果分配的内存没有被正确释放,将导致内存泄漏。
- 悬挂指针:释放内存后,指针未被设置为
nullptr
,指针仍然指向已释放的内存,成为悬挂指针。 - 重复删除:使用
delete
释放内存后,不应再次删除同一指针,这可能导致未定义行为。
动态内存分配的使用场景
- 对象大小未知:在程序运行时才能确定需要创建多少对象。
- 对象生存期跨越多个函数调用:需要在函数外部维护对象状态。
动态内存分配的优缺点
- 优点:
- 灵活性高,可以根据需要分配内存。
- 有效利用内存,只在必要时分配。
- 缺点:
- 需要手动管理内存,增加了复杂性。
- 容易出错,如内存泄漏和悬挂指针。
动态内存分配是C++中处理复杂内存管理任务的重要工具,它为程序员提供了更大的控制权,但也需要谨慎使用,以确保内存的正确管理和释放。
23. 类的继承与派生
继承的基本概念
- 定义:继承是一种机制,允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和方法。
- 目的:实现代码复用、建立层次关系、扩展现有类。
派生类的特点
- 继承基类成员:派生类继承基类的公有和保护成员(取决于继承方式)。
- 添加新成员:派生类可以添加新的成员变量和方法,扩展基类的功能。
- 重定义基类方法:派生类可以重定义(覆盖)基类的虚函数,改变其行为。
继承方式
- 公有继承(
public
):基类的公有和保护成员在派生类中保持原有的访问权限。 - 私有继承(
private
):基类的公有和保护成员在派生类中成为私有成员。 - 保护继承(
protected
):基类的公有和保护成员在派生类中成为保护成员。
派生类的构造与析构
- 构造函数:派生类必须在构造函数中初始化其新增的成员,并且可以调用基类的构造函数来初始化继承的成员。
- 析构函数:派生类的析构函数通常需要显式定义,以确保正确地清理派生类和基类的资源。
示例
class Base {
public:
Base() {
// 基类的构造代码
}
virtual void show() {
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base {
public:
Derived() : Base() {
// 派生类的构造代码,调用基类的构造函数
}
void show() override {
// 重写基类的show方法
std::cout << "Derived show" << std::endl;
}
};
int main() {
Derived d;
d.show(); // 调用派生类的show方法
return 0;
}
注意事项
- 二义性问题:如果派生类从多个基类继承,且基类中有同名成员,需要明确指出成员的访问路径。
- 虚基类:在多继承的情况下,使用虚基类可以避免基类的多重继承问题。
继承是面向对象编程的核心概念之一,它允许程序员创建层次结构的类,并在现有类的基础上构建新的功能。正确使用继承可以提高代码的复用性、可维护性和扩展性。
24. 继承方式
继承方式的概念
- 定义:继承方式定义了派生类如何从基类继承成员,以及这些成员在派生类中的访问权限。
公有继承(Public Inheritance)
- 访问权限:基类的公有成员和保护成员在派生类中保持原有的访问权限,即成为派生类的公有成员。
- 特点:派生类的对象可以访问从基类继承来的公有成员。
私有继承(Private Inheritance)
- 访问权限:基类的公有成员和保护成员在派生类中成为派生类的私有成员。
- 特点:派生类的对象不能直接访问这些成员,但派生类的成员函数可以。
保护继承(Protected Inheritance)
- 访问权限:基类的公有成员和保护成员在派生类中成为派生类的保护成员。
- 特点:派生类的对象不能直接访问这些成员,但派生类的成员函数和派生类的派生类可以访问。
继承方式的区别
- 公有继承:适合于“是一个(is-a)”关系,强调派生类是基类的一种特殊类型。
- 私有继承:适合于“使用了一个(has-a)”关系,强调派生类使用基类的功能,但基类的接口对于外部是隐藏的。
- 保护继承:较少使用,它允许派生类的成员函数访问基类的成员,同时允许进一步派生。
示例
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class PublicDerived : public Base {
// 继承方式示例
};
class PrivateDerived : private Base {
// 继承方式示例
};
class ProtectedDerived : protected Base {
// 继承方式示例
};
注意事项
- 继承方式的选择:应根据实际的类关系和设计需求来选择合适的继承方式。
- 访问控制:不同的继承方式对类成员的访问控制有不同的影响,这可能会影响类的封装性和可维护性。
继承方式是面向对象编程中重要的概念,它影响着类之间的关系和成员的可见性。合理地使用继承方式有助于构建清晰、灵活的类层次结构。
25. 类型兼容规则
类型兼容规则的概念
- 定义:类型兼容规则允许基类指针或引用可以指向派生类的对象,这是因为派生类对象“是”基类对象的更具体形式。
类型兼容规则的应用
- 向上转型(Upcasting):将派生类对象赋值给基类指针或引用,通常不需要任何特殊操作,是安全的。
- 向下转型(Downcasting):将基类指针或引用赋值给派生类指针或引用,需要进行类型转换,且可能需要进行类型检查。
多态性与类型兼容
- 多态性:允许同一个接口接受不同的数据类型。
- 动态多态:在运行时通过虚函数机制实现,基类指针可以调用派生类重写的虚函数。
示例
class Base {
public:
virtual void show() { std::cout << "Base show" << std::endl; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived show" << std::endl; }
};
int main() {
Base* basePtr = new Derived(); // 向上转型,类型兼容
basePtr->show(); // 动态绑定,调用Derived::show
// 向下转型需要类型转换,并且可能不安全
// Derived* derivedPtr = (Derived*)basePtr; // 需要检查是否为安全转换
delete basePtr;
return 0;
}
注意事项
- 类型安全:向下转型时,如果基类指针实际上没有指向派生类对象,将导致未定义行为。
- 虚函数:基类中使用虚函数可以确保通过基类指针调用正确的派生类函数。
类型兼容规则的扩展
- 菱形继承:当多个派生类继承自同一个基类时,类型兼容规则和虚继承可以解决二义性问题。
类型兼容规则是多态性的基础,它允许程序在运行时根据对象的实际类型调用相应的函数,提高了程序的灵活性和可扩展性。正确理解和应用类型兼容规则对于设计和实现多态性至关重要。
26. 派生类的构造/析构函数
派生类构造函数的特点
- 初始化继承成员:派生类构造函数负责初始化从基类继承的成员,以及派生类自己的成员。
- 构造函数链:如果派生类有多个基类,构造函数将按照继承列表中的顺序调用基类的构造函数。
- 初始化列表:使用初始化列表来初始化成员变量,包括基类成员和派生类成员。
派生类构造函数的使用方法
class Base {
public:
Base(int x) : value(x) {}
int value;
};
class Derived : public Base {
public:
Derived(int x, int y) : Base(x), y_(y) {}
int y_;
};
派生类析构函数的特点
- 清理资源:派生类析构函数负责清理派生类分配的资源,但不自动调用基类的析构函数。
- 调用顺序:析构函数的调用顺序与构造函数相反,即先调用派生类的析构函数,再调用基类的析构函数。
派生类析构函数的使用方法
class Derived {
public:
~Derived() {
// 清理派生类资源
}
};
注意事项
- 基类的默认构造函数:如果基类没有默认构造函数,派生类必须在构造函数中显式调用基类的某个构造函数。
- 构造函数的可见性:派生类构造函数的访问权限应该至少与基类成员的访问权限一致。
- 析构函数的声明:即使基类已经定义了析构函数,派生类也可能需要定义自己的析构函数来清理资源。
示例
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived() : Base() {
std::cout << "Derived constructor" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Derived d;
// 输出顺序:Base constructor, Derived constructor
// 析构顺序:Derived destructor, Base destructor
return 0;
}
派生类的构造函数和析构函数是面向对象编程中管理对象生命周期的重要部分。它们确保对象在创建和销毁时,所有必要的初始化和清理工作都能得到妥善处理。正确地实现这些函数对于防止资源泄漏和确保程序稳定性至关重要。
27. 类派生层次的作用域
派生类作用域的概念
- 定义:在类派生层次中,作用域规则决定了派生类如何访问基类成员,以及派生类成员如何影响基类成员的可见性。
作用域规则
- 基类成员的作用域:基类中的公有和保护成员在派生类中仍然保持原有的访问权限。
- 同名成员隐藏:如果派生类声明了与基类同名的成员,那么在派生类中,该成员将隐藏基类中的同名成员。
- 派生类成员的作用域:派生类中的新成员具有类作用域,可以被派生类的对象访问。
- 作用域操作符:使用
::
作用域操作符可以访问被隐藏的基类成员。
示例
class Base {
public:
int value;
};
class Derived : public Base {
public:
int value; // 隐藏了基类中的value成员
};
int main() {
Derived d;
d.value = 10; // 访问Derived类中的value成员
std::cout << d.Base::value << std::endl; // 使用作用域操作符访问Base类中的value成员
return 0;
}
注意事项
- 访问控制:即使基类的成员在派生类中可见,它们的访问权限(公有、保护、私有)仍然受到限制。
- 名称查找:在派生类中,成员名称查找首先在派生类自身中进行,然后才是基类。
- 构造函数和析构函数:基类的构造函数和析构函数在派生类对象的创建和销毁时自动调用。
作用域与继承方式
- 公有继承:基类中的公有成员和保护成员在派生类中保持原有的访问权限。
- 私有继承:基类中的公有和保护成员在派生类中变为私有成员,不能被派生类的对象直接访问。
- 保护继承:基类中的公有和保护成员在派生类中变为保护成员,只能被派生类及其派生类的成员访问。
理解派生类中作用域的规则对于正确地访问和管理类成员至关重要。这些规则确保了类的封装性和继承的合理性,同时允许程序员在派生类中适当地扩展和重用基类的功能。
28. 二义性问题
二义性问题的概念
- 定义:在派生类中,如果存在多个基类有同名成员,或者基类之间存在继承关系,导致派生类从多个路径继承到同一个基类,这种情况下对成员的访问可能产生二义性。
解决二义性问题的方法
- 作用域操作符:使用作用域操作符
::
来明确指定要访问的成员属于哪个基类。 - 虚基类:如果二义性问题是由于多个基类继承自同一个基类导致的,可以将这个共同基类声明为虚基类,以确保派生类中只有一个该基类的实例。
示例
class Base {
public:
int value;
};
class Derived1 : public Base {
public:
void show() {
std::cout << value << std::endl; // 二义性问题
}
};
class Derived2 : public Base {
public:
void display() {
std::cout << ::Base::value << std::endl; // 使用作用域操作符解决二义性
}
};
int main() {
Derived1 d1;
d1.show(); // 错误:二义性
Derived2 d2;
d2.display(); // 正确:使用作用域操作符
return 0;
}
注意事项
- 明确性:在访问可能产生二义性的成员时,应该明确指出成员所属的类。
- 虚基类:使用虚基类可以避免在派生类中多次继承同一个基类的成员,减少二义性问题。
虚基类的使用方法
- 声明:在派生类中,通过在继承列表中使用
virtual
关键字来声明虚基类。 - 构造函数:在派生类的构造函数中,需要显式调用虚基类的构造函数来初始化。
示例
class Base {
public:
Base(int x) : value(x) {}
int value;
};
class Derived : virtual Base {
public:
Derived(int x) : Base(x) {} // 显式调用虚基类的构造函数
};
int main() {
Derived d(10);
std::cout << d.value << std::endl; // 正确:没有二义性
return 0;
}
二义性问题是面向对象编程中常见的问题之一,特别是在使用多继承时。掌握如何解决二义性问题对于编写清晰、可维护的代码至关重要。通过使用作用域操作符和虚基类,可以有效地解决这些问题。
29. 虚基类
虚基类的概念
- 定义:虚基类是一种特殊的基类,用于解决多继承体系中的二义性问题和基类成员的多重继承问题。
虚基类的作用
- 解决二义性:当多个基类继承自同一个基类时,如果不使用虚基类,派生类可能会从多个路径继承到相同的基类成员,导致二义性。
- 避免多重继承:虚基类确保派生类中只有一个该基类的实例,避免了数据成员的重复。
虚基类的声明
- 语法:在类继承列表中使用
virtual
关键字来声明虚基类。class Derived : public virtual Base { // ... };
虚基类的构造函数
- 调用顺序:在派生类的构造函数中,虚基类的构造函数会首先被调用,然后是其他基类的构造函数。
虚基类的析构函数
- 调用顺序:与构造函数相反,析构函数首先调用派生类的析构函数,然后是其他基类的析构函数,最后调用虚基类的析构函数。
示例
class Base {
public:
Base(int x) : value(x) {}
virtual ~Base() {} // 虚析构函数
int value;
};
class Intermediate : public virtual Base {
// ...
};
class Derived : public Intermediate {
public:
Derived(int x) : Base(x), Intermediate() {
// Base的构造函数首先被调用
}
~Derived() {
// 析构顺序与构造顺序相反
}
};
int main() {
Derived d(10);
return 0;
}
注意事项
- 虚析构函数:如果基类中有虚析构函数,确保在派生类中调用基类的析构函数。
- 构造函数的初始化列表:在派生类的构造函数中,需要在初始化列表中显式调用虚基类的构造函数。
虚基类是C++中处理多继承问题的一种有效机制。通过使用虚基类,可以减少派生类中的冗余基类成员,同时解决由于多路径继承引起的二义性问题。正确地使用虚基类对于设计清晰、高效的类继承结构至关重要。
30. 多态性概念
多态性的定义
- 概念:多态性(Polymorphism)是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。
多态性的实现方式
-
重载(Overloading):
- 函数名相同,但参数列表不同(参数的类型、数量或顺序至少有一项不同)。
- 编译时根据调用的上下文确定具体调用哪个函数。
-
重写(Overriding):
- 派生类中的方法与基类中的方法具有相同的函数名、参数列表和返回类型。
- 运行时根据对象的实际类型调用相应的方法。
-
虚函数(Virtual Functions):
- 在基类中使用
virtual
关键字声明的函数,可以在派生类中被重写。 - 通过基类的指针或引用调用虚函数时,将调用对象实际类型的重写方法。
- 在基类中使用
多态性的使用场景
- 接口统一:当需要对不同类型的对象执行相同操作时,可以使用多态性。
- 扩展性:在不修改现有代码的情况下,通过继承和重写方法来扩展功能。
示例
class Shape {
public:
virtual void draw() { std::cout << "Drawing a shape" << std::endl; }
virtual ~Shape() {} // 虚析构函数
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing a circle" << std::endl; }
};
class Square : public Shape {
public:
void draw() override { std::cout << "Drawing a square" << std::endl; }
};
int main() {
Shape* shapes[] = {new Circle(), new Square()};
for (Shape* s : shapes) {
s->draw(); // 多态性:调用实际对象类型的draw方法
}
for (Shape* s : shapes) {
delete s;
}
return 0;
}
注意事项
- 虚析构函数:如果类中有虚函数,通常需要声明一个虚析构函数,以确保派生类对象被删除时,正确的析构函数被调用。
- 纯虚函数和抽象类:如果类中有一个或多个纯虚函数(只有声明没有定义的虚函数),则该类成为抽象类,不能实例化。
多态性是面向对象编程的核心概念之一,它提高了程序的灵活性和可扩展性。通过多态性,可以编写通用的代码来处理不同类型的对象,而具体的实现则由对象的实际类型决定。
31. 运算符重载
运算符重载的概念
- 定义:运算符重载(Operator Overloading)允许程序员为自定义类型(如类和结构体)定义运算符的行为,使得这些类型可以使用标准的运算符。
运算符重载的目的
- 一致性:使得自定义类型能够使用语言提供的运算符,提供一致的编程体验。
- 可读性:提高代码的可读性,使得对自定义类型的操作更接近自然语言。
运算符重载的方法
- 成员函数:将运算符函数定义为类的成员函数。
- 友元函数:将运算符函数定义为类的友元函数,以便访问类的私有成员。
示例
class Complex {
public:
double real, imag;
// 成员函数重载
Complex operator+(const Complex& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
// 友元函数重载
friend Complex operator-(const Complex& lhs, const Complex& rhs);
};
Complex operator-(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.real - rhs.real, lhs.imag - rhs.imag);
}
注意事项
- 限制:有些运算符不能被重载,如
.
(成员访问)、::
(作用域解析)、? :
(三元运算符)等。 - 运算符函数:运算符函数可以是成员函数或友元函数,但必须具有至少一个参数。
- 隐式转换:运算符重载可以参与隐式类型转换,但应谨慎使用,以避免不清晰的代码。
运算符重载的应用
- 自定义类型:为自定义类型提供加、减、乘、除等运算符的行为定义。
- 容器类:为容器类定义下标访问
[]
、赋值=
、比较==
和!=
等运算符。
示例
Complex c1(1.0, 2.0), c2(3.0, 4.0);
Complex c3 = c1 + c2; // 调用成员函数重载
Complex c4 = c1 - c2; // 调用友元函数重载
运算符重载是C++中一种强大的特性,它允许程序员扩展语言的运算符,以适应自定义类型的需求。正确使用运算符重载可以提高代码的表达力和易用性,但也需要谨慎处理,以避免混淆和潜在的错误。
32. 动态联编与虚函数
动态联编的概念
- 定义:动态联编(Dynamic Binding),又称为运行时多态,是指在程序运行时才确定调用哪个函数的行为。
- 原理:通过虚函数机制实现,程序在执行时根据对象的实际类型来调用相应的函数。
虚函数的作用
- 实现多态性:虚函数允许在基类指针或引用上调用派生类中重写的方法,实现动态联编。
- 提供接口:为派生类提供一个可以被重写的接口,增加代码的灵活性和可扩展性。
虚函数的使用方法
- 声明虚函数:在基类中使用
virtual
关键字声明函数,使其成为虚函数。 - 重写虚函数:在派生类中对基类的虚函数进行重写,提供具体的实现。
- 调用虚函数:通过基类的指针或引用调用虚函数,程序运行时将调用对象实际类型的函数。
示例
class Base {
public:
virtual void show() { std::cout << "Base show" << std::endl; }
virtual ~Base() {} // 虚析构函数
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived show" << std::endl; }
};
int main() {
Base* basePtr = new Derived();
basePtr->show(); // 动态联编:调用Derived::show
delete basePtr;
return 0;
}
注意事项
- 虚析构函数:如果类中有虚函数,通常需要声明一个虚析构函数,以确保在删除通过基类指针指向的派生类对象时,正确地调用派生类的析构函数。
- 纯虚函数和抽象类:如果类中有一个或多个纯虚函数(没有函数体的虚函数),则该类成为抽象类,不能被实例化。
动态联编的优点
- 解耦合:基类和派生类可以在不同的编译单元中独立开发和维护。
- 扩展性:可以引入新的派生类而不需要修改基类或使用基类的客户端代码。
动态联编和虚函数是实现运行时多态的关键机制,它们使得程序能够根据对象的实际类型来调用相应的函数,从而提高了代码的灵活性和可维护性。正确使用虚函数对于设计可扩展的面向对象系统至关重要。
33. 纯虚函数与抽象类
纯虚函数的概念
- 定义:纯虚函数(Pure Virtual Function)是在基类中声明的虚函数,它没有具体的实现,仅提供一个接口。
- 声明方式:在函数声明的末尾使用
= 0
(在C++98及之前的版本)或= delete
(在C++11及之后的版本)。
纯虚函数的作用
- 强制抽象类:纯虚函数使得基类成为一个抽象类,不能被直接实例化。
- 规范派生类:为派生类提供一个必须实现的接口,确保派生类具有特定的行为。
示例
class Shape {
public:
virtual void draw() = 0; // 纯虚函数,声明在C++98
// 或者
virtual void resize() = delete; // 声明在C++11,不允许重写
virtual ~Shape() {} // 虚析构函数
};
抽象类的概念
- 定义:抽象类(Abstract Class)是指包含至少一个纯虚函数的类。
- 特点:不能被实例化,但可以被继承。
抽象类的应用
- 模板:为派生类提供一个公共的模板,定义所有派生类必须遵守的接口。
- 扩展性:允许在不修改现有代码的基础上引入新的派生类。
示例
class Animal {
public:
virtual void makeSound() = 0; // 纯虚函数
};
class Dog : public Animal {
public:
void makeSound() override { std::cout << "Woof!" << std::endl; }
};
int main() {
// Animal animal; // 错误:Animal是抽象类,不能实例化
Dog dog;
dog.makeSound(); // 正确:调用Dog类的makeSound方法
return 0;
}
注意事项
- 接口规范:纯虚函数通常没有函数体,只提供接口规范。
- 派生类实现:派生类必须实现基类中的所有纯虚函数,除非派生类也是抽象类。
- 虚析构函数:在包含纯虚函数的基类中,建议声明虚析构函数,以确保派生类的析构函数被正确调用。
纯虚函数和抽象类是面向对象编程中实现接口规范和设计灵活性的重要工具。通过使用纯虚函数,可以定义一个必须由派生类实现的接口,而抽象类则提供了一种机制,确保类作为接口存在,不被直接实例化。这有助于构建清晰、可维护的软件架构。
编程样例
样例1
知识点
- 初识C++程序
- 掌握C++程序的基本结构。
- 理解注释的使用。
- 掌握预处理器编译指令,特别是
#include
。
- C++输入输出
- 熟练使用
cin
和cout
进行输入输出操作。
- 熟练使用
- C++定义符号常量
- 理解使用
const
定义符号常量,代替#define
。
- 理解使用
- 预处理指令
- 掌握如何使用预处理指令包含头文件。
- 函数定义和声明
- 理解C语言中的函数定义和声明。
- 函数调用
- 理解C语言中的函数调用机制。
以下是一个C++程序的样例代码,以及对代码架构的解释:
// Example.cpp
#include <iostream> // 4. 预处理指令,包含标准输入输出流库
// 3. 使用const定义符号常量,代替#define
const double PI = 3.14159;
// 5. 函数定义和声明
// 声明一个函数,该函数计算圆的面积
double calculateCircleArea(double radius);
// 1. 初识C++程序
// 主函数是每个C++程序的入口点
int main() {
// 2. 使用cin和cout进行输入输出操作
std::cout << "Enter the radius of the circle: ";
double radius;
std::cin >> radius;
// 调用函数并显示结果
double area = calculateCircleArea(radius);
std::cout << "The area of the circle is: " << area << std::endl;
return 0;
}
// 5. 函数定义
// 定义前面声明的函数,计算并返回圆的面积
double calculateCircleArea(double radius) {
return PI * radius * radius; // 使用符号常量PI
}
代码架构解析:
-
预处理器指令 (
#include <iostream>
):- 这行告诉编译器包含标准输入输出流库的头文件,这样我们就可以在我们的程序中使用
std::cout
和std::cin
。
- 这行告诉编译器包含标准输入输出流库的头文件,这样我们就可以在我们的程序中使用
-
符号常量定义 (
const double PI = 3.14159;
):- 这里使用
const
关键字定义了一个符号常量PI
,它用于存储圆周率的值。这比宏定义(#define
)更好,因为它有数据类型,编译器会在编译时检查类型安全。
- 这里使用
-
函数的声明 (
double calculateCircleArea(double radius);
):- 在
main
函数之前声明了calculateCircleArea
函数,这允许我们在main
函数中调用它,尽管它在后面才被定义。
- 在
-
主函数 (
int main() { ... }
):main
函数是程序的入口点。程序执行从这里开始。
-
输入输出操作 (
std::cout
和std::cin
):- 使用
std::cout
输出提示信息到控制台,使用std::cin
从控制台读取用户输入的半径值。
- 使用
-
函数调用 (
double area = calculateCircleArea(radius);
):- 在
main
函数中调用了之前声明的calculateCircleArea
函数,并使用返回的面积值。
- 在
-
函数定义 (
double calculateCircleArea(double radius) { ... }
):- 这是
calculateCircleArea
函数的定义,它计算并返回圆的面积。函数体内使用了之前定义的PI
常量。
- 这是
这个样例程序展示了C++程序的基本结构,包括预处理指令的使用、符号常量的声明、函数的声明与定义,以及输入输出操作。通过这个样例,您可以了解如何编写一个简单的C++程序,并逐步扩展更复杂的逻辑。
样例2
知识点
- 值传递形参
- 注意值传递与引用传递的区别。
- 引用传递形参
- 掌握引用传递的概念和使用。
- 带默认形参值的函数
- 理解如何为函数参数设置默认值。
以下是一个C++程序样例,它涵盖了值传递形参、引用传递形参以及带默认形参值的函数的知识点,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 函数示例1:值传递
void valuePassing(int n) {
n += 10; // 修改n的值,但不会影响实参
}
// 函数示例2:引用传递
void referencePassing(int &r) {
r += 20; // 通过引用修改实参的值
}
// 函数示例3:带默认参数值的函数
void printMessage(const string &msg, const string &appender = "!") {
cout << msg << appender << endl;
}
int main() {
int a = 5;
string greeting = "Hello";
// 演示值传递
cout << "Value passing before: " << a << endl;
valuePassing(a);
cout << "Value passing after: " << a << endl; // a的值不变
// 演示引用传递
cout << "Reference passing before: " << a << endl;
referencePassing(a);
cout << "Reference passing after: " << a << endl; // a的值改变
// 演示带默认参数值的函数
printMessage(greeting); // 使用默认的附加字符串
printMessage(greeting, "!!!"); // 使用自定义的附加字符串
return 0;
}
代码架构解析:
-
值传递函数 (
valuePassing
):- 此函数接收一个
int
类型的参数n
,通过值传递的方式。函数内部对n
的修改不会影响到实参。
- 此函数接收一个
-
引用传递函数 (
referencePassing
):- 此函数接收一个
int
类型的引用参数r
,通过引用传递的方式。函数内部对r
的修改会影响到实参。
- 此函数接收一个
-
带默认参数值的函数 (
printMessage
):- 此函数接收两个
string
类型的参数,其中第二个参数appender
有默认值"!"
。如果调用时不提供第二个参数,将使用默认值。
- 此函数接收两个
-
main
函数:- 程序的入口点,声明了两个变量
a
和greeting
,用于演示值传递和引用传递的效果。 - 调用
valuePassing
函数展示值传递,调用referencePassing
函数展示引用传递。 - 调用
printMessage
函数两次,一次使用默认参数,一次使用自定义参数。
- 程序的入口点,声明了两个变量
-
输入输出 (
cout
):- 使用
cout
来显示变量的值变化和消息。
- 使用
这个样例程序清晰地展示了值传递和引用传递的区别,以及如何为函数参数设置默认值。通过运行这个程序,您可以直观地看到每种传递方式对实参的影响。
样例3
知识点
- 类的定义
- 掌握类的基本定义方法。 - 对象的定义与使用
- 理解如何定义和使用对象。
以下是一个C++程序样例,它涵盖了类的定义和对象的使用,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 10. 类的定义
// 定义一个简单的类Person
class Person {
private:
string name; // 私有成员变量,存储人的名字
int age; // 私有成员变量,存储人的年龄
public:
// 类的构造函数
Person(const string& name, int age) : name(name), age(age) {}
// 成员函数,用于输出Person的信息
void displayInfo() const {
cout << "Name: " << name << ", Age: " << age << endl;
}
// 成员函数,用于设置Person的名字
void setName(const string& newName) {
name = newName;
}
// 成员函数,用于设置Person的年龄
void setAge(int newAge) {
age = newAge;
}
};
// 11. 对象的定义与使用
int main() {
// 定义Person类的对象
Person person1("Alice", 30); // 使用构造函数创建对象
// 使用成员函数displayInfo输出对象信息
person1.displayInfo();
// 改变对象的状态
person1.setName("Bob");
person1.setAge(25);
// 再次使用成员函数displayInfo输出更新后的对象信息
person1.displayInfo();
return 0;
}
代码架构解析:
-
类的定义 (
class Person
):- 定义了一个名为
Person
的类,包含私有成员变量name
和age
,以及公共成员函数。
- 定义了一个名为
-
构造函数 (
Person(const string& name, int age)
):Person
类的构造函数,用于初始化对象的name
和age
。
-
成员函数 (
displayInfo
,setName
,setAge
):displayInfo
:输出Person的信息。setName
:设置Person的名字。setAge
:设置Person的年龄。
-
main
函数:- 程序的入口点,定义了一个
Person
类的对象person1
,并使用构造函数初始化。 - 调用
displayInfo
成员函数来显示person1
的信息。 - 使用
setName
和setAge
成员函数来更新person1
的状态。 - 再次调用
displayInfo
来显示更新后的信息。
- 程序的入口点,定义了一个
-
对象的定义与使用:
person1
是Person
类的一个对象实例,展示了如何创建和使用类的对象。
这个样例程序展示了如何在C++中定义一个类,以及如何创建和操作类的对象。通过这个程序,您可以学习到类的基本结构、成员变量、构造函数以及成员函数的使用。
样例4
知识点
- 构造函数、复制构造函数
- 理解构造函数和复制构造函数的作用。 - 构造函数的初始化列表
- 掌握使用初始化列表对成员变量进行初始化。
以下是一个C++程序样例,它涵盖了构造函数、复制构造函数以及构造函数的初始化列表的使用,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 定义一个简单的类Box,表示一个立方体盒子
class Box {
private:
double length; // 长度
double width; // 宽度
double height; // 高度
public:
// 12. 构造函数
// 使用初始化列表对成员变量进行初始化
Box(double l = 1.0, double w = 1.0, double h = 1.0)
: length(l), width(w), height(h) {
cout << "Box constructed with length " << length
<< ", width " << width << ", and height " << height << endl;
}
// 复制构造函数
Box(const Box& other)
: length(other.length), width(other.width), height(other.height) {
cout << "Copy constructor called." << endl;
}
// 成员函数,用于输出盒子的尺寸
void displayDimensions() const {
cout << "Box dimensions: length = " << length
<< ", width = " << width << ", height = " << height << endl;
}
};
int main() {
// 使用默认参数创建一个Box对象
Box box1(2.0, 3.0, 4.0);
box1.displayDimensions();
// 使用复制构造函数创建box1的副本
Box box2 = box1; // 调用复制构造函数
box2.displayDimensions();
return 0;
}
代码架构解析:
-
类定义 (
class Box
):- 定义了一个名为
Box
的类,包含私有成员变量length
、width
和height
。
- 定义了一个名为
-
构造函数 (
Box(double l = 1.0, double w = 1.0, double h = 1.0)
):Box
类的构造函数,使用默认参数和初始化列表对成员变量进行初始化,并输出构造时的信息。
-
复制构造函数 (
Box(const Box& other)
):Box
类的复制构造函数,使用初始化列表复制另一个Box
对象的成员变量,并输出复制构造函数被调用的信息。
-
成员函数 (
displayDimensions
):- 一个用于输出盒子尺寸的成员函数。
-
main
函数:- 程序的入口点,创建了一个
Box
类的对象box1
,并显示其尺寸。 - 使用复制构造函数创建了
box1
的副本box2
,并显示其尺寸。
- 程序的入口点,创建了一个
-
对象的创建和复制:
box1
是直接使用构造函数创建的,而box2
是通过复制构造函数从box1
复制得到的。
这个样例程序展示了构造函数和复制构造函数的使用,以及如何利用构造函数的初始化列表来初始化对象的成员变量。通过运行这个程序,您可以直观地看到构造函数和复制构造函数的调用和它们的作用。
样例 5:基础类和成员
知识点
- 14. 前向引用声明:展示如何在类定义中提前声明其他类。
- 15. 命名空间:演示如何使用命名空间,包括
using
声明和using namespace
指令。 - 16. 静态成员:定义一个类,包含静态成员变量和静态成员函数。
以下是一个C++程序样例,它涵盖了前向引用声明、命名空间的使用以及静态成员的概念,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 15. 命名空间的使用
namespace MyNamespace {
class MyClass; // 14. 前向引用声明
class MyOtherClass {
public:
void display() {
cout << "MyOtherClass display function." << endl;
}
};
}
class MyClass {
public:
void display() {
cout << "MyClass display function." << endl;
}
// 16. 静态成员变量
static int count;
// 16. 静态成员函数
static void staticFunction() {
cout << "Static function in MyClass." << endl;
}
};
// 16. 静态成员变量的定义
int MyClass::count = 0;
void useMyClasses() {
MyNamespace::MyOtherClass other;
MyClass myClass;
other.display();
myClass.display();
cout << "MyClass object count: " << MyClass::count << endl; // 16. 使用静态成员变量
MyClass::staticFunction(); // 16. 调用静态成员函数
}
int main() {
// 使用using声明来使用MyNamespace中的MyClass和MyOtherClass
using MyNamespace::MyClass;
using MyNamespace::MyOtherClass;
MyClass myClassInstance;
MyOtherClass otherClassInstance;
myClassInstance.display();
otherClassInstance.display();
useMyClasses();
return 0;
}
代码架构解析:
-
命名空间声明 (
namespace MyNamespace
):- 定义了一个命名空间
MyNamespace
,用于封装类MyClass
和MyOtherClass
。
- 定义了一个命名空间
-
前向引用声明 (
class MyClass;
):- 在
MyNamespace
命名空间中提前声明了MyClass
类,这是一个前向引用声明。
- 在
-
类定义 (
class MyClass
和class MyOtherClass
):- 定义了
MyClass
和MyOtherClass
两个类,包含成员函数display
用于输出信息。
- 定义了
-
静态成员变量 (
static int count
):- 在
MyClass
中定义了一个静态成员变量count
,用于跟踪MyClass
对象的数量。
- 在
-
静态成员函数 (
static void staticFunction()
):- 在
MyClass
中定义了一个静态成员函数staticFunction
,可以被类直接调用而不需要对象实例。
- 在
-
静态成员变量的定义 (
int MyClass::count = 0;
):- 在类定义外部,为静态成员变量
count
提供了定义和初始化。
- 在类定义外部,为静态成员变量
-
使用using声明 (
using MyNamespace::MyClass; using MyNamespace::MyOtherClass;
):- 在
main
函数中使用using
声明来引入MyNamespace
命名空间中的MyClass
和MyOtherClass
,使得可以直接使用这些类而不需要命名空间前缀。
- 在
-
main
函数:- 程序的入口点,创建了
MyClass
和MyOtherClass
的对象,并调用它们的display
成员函数。 - 调用了
useMyClasses
函数,演示了如何使用静态成员变量和静态成员函数。
- 程序的入口点,创建了
-
useMyClasses
函数:- 演示了如何不使用命名空间前缀直接使用类,以及如何使用静态成员。
这个样例程序展示了如何在C++中使用命名空间来组织代码,如何进行前向引用声明,以及静态成员的使用。通过这个程序,您可以学习到如何在实际编程中应用这些概念。
样例 6:高级类特性
知识点
- 17. 友元函数:创建一个类和友元函数,展示友元函数如何访问类的私有成员。
- 18. 常成员函数:定义常成员函数,展示它们如何在不修改对象状态的情况下操作。
以下是一个C++程序样例,它涵盖了友元函数和常成员函数的概念,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 定义一个简单的类Box
class Box {
private:
int length;
int width;
int height;
public:
// 构造函数
Box(int l, int w, int h) : length(l), width(w), height(h) {}
// 18. 常成员函数:getVolume
// 这个函数被声明为const,意味着它不会修改对象的状态
int getVolume() const {
return length * width * height;
}
// 声明友元函数
friend int getSurfaceArea(const Box&);
};
// 17. 友元函数:getSurfaceArea
// 这个函数不是Box类的成员函数,但可以访问Box类的私有成员
int getSurfaceArea(const Box& box) {
return 2 * (box.length * box.width + box.width * box.height + box.height * box.length);
}
int main() {
Box box(10, 20, 30); // 创建Box对象
// 调用常成员函数getVolume
cout << "Volume of the box: " << box.getVolume() << endl;
// 友元函数getSurfaceArea可以访问box的私有成员
cout << "Surface area of the box: " << getSurfaceArea(box) << endl;
return 0;
}
代码架构解析:
-
类定义 (
class Box
):- 定义了一个名为
Box
的类,包含私有成员变量length
、width
和height
。
- 定义了一个名为
-
构造函数 (
Box(int l, int w, int h)
):Box
类的构造函数,用于初始化对象的尺寸。
-
常成员函数 (
int getVolume() const
):getVolume
函数被声明为const
,表示它是一个常成员函数,不会修改对象的状态。它计算并返回盒子的体积。
-
友元函数声明:
- 在
Box
类中,使用friend
关键字声明getSurfaceArea
函数为友元函数,允许它访问Box
类的私有成员。
- 在
-
友元函数定义 (
int getSurfaceArea(const Box& box)
):getSurfaceArea
函数计算并返回盒子的表面积。尽管它不是Box
类的成员函数,但由于被声明为友元,它可以访问Box
类的私有成员。
-
main
函数:- 程序的入口点,创建了一个
Box
类的对象box
。 - 调用
getVolume
常成员函数来获取并输出盒子的体积。 - 调用友元函数
getSurfaceArea
来获取并输出盒子的表面积。
- 程序的入口点,创建了一个
-
友元函数的使用:
- 在
main
函数中,通过getSurfaceArea(box)
调用友元函数,展示了友元函数如何与类的对象交互,即使它不是类的成员函数。
- 在
这个样例程序展示了如何在C++中使用友元函数来访问类的私有成员,以及如何定义和使用常成员函数。通过这个程序,您可以学习到友元函数和常成员函数在实际编程中的应用。
样例 7:指针和this
知识点
- 19. 对象指针:使用对象指针来操作类的对象。
- 20. this指针:在成员函数中使用
this
指针,展示它如何指向当前对象。 - 21. 指向成员的指针:定义并使用指向类成员的指针,包括数据成员和成员函数。
以下是一个C++程序样例,它涵盖了对象指针、this
指针以及指向类成员的指针的使用,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 定义一个简单的类Person
class Person {
private:
string name;
int age;
public:
// 构造函数
Person(const string& nm, int ag) : name(nm), age(ag) {}
// 使用this指针展示数据成员的初始值
void showInitialInfo() {
cout << "Name: " << this->name << ", Age: " << this->age << endl;
}
// 成员函数,使用对象指针操作数据成员
void introduce(Person* p) {
cout << "I am " << p->name << ", and I am " << p->age << " years old." << endl;
}
// 指向数据成员的指针
string* getNamePtr() { return &this->name; }
int* getAgePtr() { return &this->age; }
// 指向成员函数的指针
using FunPtr = void (Person::*)(const string&, int);
FunPtr getMemberFuncPtr() {
return &Person::setNameAndAge;
}
void setNameAndAge(const string& newName, int newAge) {
name = newName;
age = newAge;
cout << "Name and age updated to: " << name << ", " << age << endl;
}
};
int main() {
Person person("Alice", 30);
// 使用对象指针调用成员函数
person.introduce(&person);
// 使用this指针
person.showInitialInfo();
// 使用指向成员的指针
string* namePtr = person.getNamePtr();
int* agePtr = person.getAgePtr();
cout << "Name ptr: " << *namePtr << ", Age ptr: " << *agePtr << endl;
// 使用指向成员函数的指针
Person::FunPtr funcPtr = person.getMemberFuncPtr();
(person.*funcPtr)("Bob", 25); // 使用成员函数指针调用成员函数
return 0;
}
代码架构解析:
-
类定义 (
class Person
):- 定义了一个名为
Person
的类,包含私有数据成员name
和age
,以及多个成员函数。
- 定义了一个名为
-
构造函数 (
Person(const string& nm, int ag)
):Person
类的构造函数,用于初始化对象的名称和年龄。
-
成员函数 (
showInitialInfo
,introduce
,getNamePtr
,getAgePtr
,getMemberFuncPtr
,setNameAndAge
):showInitialInfo
:展示使用this
指针访问数据成员的初始值。introduce
:使用对象指针p
来访问和操作另一个Person
对象的数据成员。getNamePtr
和getAgePtr
:返回指向数据成员的指针。getMemberFuncPtr
:返回指向成员函数setNameAndAge
的指针。
-
main
函数:- 程序的入口点,创建了
Person
类的对象person
。 - 使用
introduce
成员函数和对象指针来操作对象。 - 调用
showInitialInfo
成员函数,展示this
指针的使用。 - 通过
getNamePtr
和getAgePtr
获取指向数据成员的指针,并输出成员值。 - 使用
getMemberFuncPtr
获取指向成员函数的指针,并调用该成员函数。
- 程序的入口点,创建了
-
对象指针的使用:
- 在
main
函数中,通过&person
获取对象的地址,并将其作为对象指针传递给introduce
成员函数。
- 在
-
this
指针的使用:- 在
showInitialInfo
成员函数中,使用this->name
和this->age
来访问当前对象的数据成员。
- 在
-
指向成员的指针:
getNamePtr
和getAgePtr
成员函数返回指向私有数据成员的指针。getMemberFuncPtr
返回一个指向成员函数的指针,然后通过这种指针调用成员函数。
这个样例程序展示了对象指针、this
指针以及指向类成员的指针的使用。通过这个程序,您可以学习到如何在C++中操作和管理类的对象,以及如何使用指针来提高程序的灵活性。
样例 8:继承和多态
知识点
- 22. 继承方式:通过单继承和多继承展示类的继承方式。
- 23. 类型兼容规则:展示如何通过基类指针或引用来操作派生类对象。
- 24. 派生类的构造/析构函数:创建派生类,并展示其构造函数和析构函数的执行。
以下是一个C++程序样例,它涵盖了继承方式、类型兼容规则以及派生类的构造和析构函数的概念,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 基类 Shape
class Shape {
public:
// 构造函数
Shape() {
cout << "Shape constructor called." << endl;
}
// 虚析构函数
virtual ~Shape() {
cout << "Shape destructor called." << endl;
}
// 虚函数,用于多态
virtual void draw() {
cout << "Drawing a shape." << endl;
}
};
// 派生类 Circle,单继承自 Shape
class Circle : public Shape {
public:
// 构造函数
Circle() {
cout << "Circle constructor called." << endl;
}
// 重写析构函数
~Circle() {
cout << "Circle destructor called." << endl;
}
// 重写虚函数 draw
void draw() override {
cout << "Drawing a circle." << endl;
}
};
// 派生类 Square,单继承自 Shape
class Square : public Shape {
public:
// 构造函数
Square() {
cout << "Square constructor called." << endl;
}
// 析构函数
~Square() {
cout << "Square destructor called." << endl;
}
// 重写虚函数 draw
void draw() override {
cout << "Drawing a square." << endl;
}
};
// 多继承示例
class CircleSquare : public Circle, public Square {
public:
CircleSquare() {
cout << "CircleSquare constructor called." << endl;
}
~CircleSquare() {
cout << "CircleSquare destructor called." << endl;
}
};
int main() {
// 使用基类指针操作派生类对象,展示类型兼容规则
Shape* shape = new Circle(); // 向上转型
shape->draw(); // 多态行为,调用 Circle::draw
delete shape; // 调用 Circle 的析构函数
shape = new Square(); // 再次向上转型
shape->draw(); // 调用 Square::draw
delete shape; // 调用 Square 的析构函数
// 多继承示例
CircleSquare* cSquare = new CircleSquare();
cSquare->draw(); // 多态行为,调用 CircleSquare 的 draw
delete cSquare; // 调用 CircleSquare 的析构函数
return 0;
}
代码架构解析:
-
基类 Shape:
- 定义了一个名为
Shape
的基类,包含构造函数、虚析构函数和虚函数draw
。
- 定义了一个名为
-
派生类 Circle 和 Square:
- 定义了两个从
Shape
基类单继承的派生类Circle
和Square
。每个类都有自己的构造函数和析构函数,并重写了draw
函数。
- 定义了两个从
-
多继承示例 CircleSquare:
- 定义了一个名为
CircleSquare
的类,它同时继承自Circle
和Square
类,展示了多继承的概念。
- 定义了一个名为
-
main
函数:- 程序的入口点,创建了
Shape
类型的指针shape
,并通过这个指针操作Circle
和Square
对象,展示了类型兼容规则。 - 通过基类指针调用派生类的
draw
函数,展示了多态行为。 - 创建了
CircleSquare
对象并调用其draw
函数,展示了多继承的使用。
- 程序的入口点,创建了
-
构造函数和析构函数的执行:
- 当创建对象时,构造函数被调用,展示了派生类构造函数的执行。
- 当删除对象时,析构函数被调用,展示了析构函数的执行顺序。
这个样例程序展示了如何在C++中使用继承和多态,以及如何通过基类指针操作派生类对象。通过这个程序,您可以学习到继承方式、类型兼容规则以及派生类的构造和析构函数的执行机制。
样例 9:高级继承和多态
知识点
- 25. 虚基类:使用虚基类来解决多继承中的二义性问题。
- 26. 动态联编与虚函数:展示动态联编如何工作,以及如何使用虚函数实现多态性。
以下是一个C++程序样例,它涵盖了虚基类和动态联编的概念,并提供了代码架构的解释:
#include <iostream>
using namespace std;
// 定义一个基类 Shape,声明为虚基类
class Shape {
public:
virtual void draw() { cout << "Drawing a shape." << endl; }
virtual ~Shape() {}
};
// 派生类 Circle,继承自虚基类 Shape
class Circle : public virtual Shape {
public:
void draw() override {
cout << "Drawing a circle." << endl;
}
};
// 派生类 Square,继承自虚基类 Shape
class Square : public virtual Shape {
public:
void draw() override {
cout << "Drawing a square." << endl;
}
};
// 多重继承类 PolyShape,同时继承自 Circle 和 Square
class PolyShape : public Circle, public Square {
public:
void draw() override {
cout << "Drawing a complex shape made of circles and squares." << endl;
}
};
int main() {
// 使用基类指针指向派生类对象,展示多态
Shape* shape = new PolyShape();
shape->draw(); // 动态联编调用 PolyShape::draw
delete shape; // 正确调用析构函数
return 0;
}
代码架构解析:
-
虚基类 Shape:
- 定义了一个名为
Shape
的基类,并声明其为虚基类。包含一个虚函数draw
和虚析构函数。
- 定义了一个名为
-
派生类 Circle 和 Square:
- 定义了两个从
Shape
虚基类派生的类Circle
和Square
。每个类都重写了draw
函数。
- 定义了两个从
-
多重继承类 PolyShape:
- 定义了一个名为
PolyShape
的类,它同时继承自Circle
和Square
。展示了多继承的情况,并重写了draw
函数。
- 定义了一个名为
-
main
函数:- 程序的入口点,创建了
Shape
类型的指针shape
,并通过这个指针操作PolyShape
对象。 - 调用
shape->draw()
展示了多态性,即使shape
是基类类型的指针,它也能调用正确的PolyShape::draw
函数。
- 程序的入口点,创建了
-
动态联编:
- 当通过基类指针调用虚函数时,C++ 运行时会确定调用哪个实际的函数版本,这个过程称为动态联编。
-
析构函数的调用:
- 在
main
函数的末尾,删除shape
指针指向的对象时,会按照构造的逆序调用析构函数,首先调用PolyShape
的析构函数,然后是Circle
和Square
的析构函数,最后调用Shape
的析构函数。
- 在
这个样例程序展示了如何在C++中使用虚基类来避免多继承中的二义性问题,以及如何通过虚函数实现多态性。通过运行这个程序,您可以直观地看到多态的工作原理和析构函数的正确调用顺序。