10.1、过程性编程和面向对象编程
过程性编程和面向对象编程是两种常见的编程范式,它们代表了不同的编程思想和方法论。
过程性编程是基于过程和函数的编程方式,强调将程序分解为小的、独立的任务,每个任务由一个或多个函数完成。在过程性编程中,数据和函数通常是分开的,函数仅仅处理传入的数据,不关心调用方和外部环境。过程性编程也称为结构化编程,是早期编程语言的主要范式。
面向对象编程则是基于对象的编程方式,强调将程序分解为一个个对象,每个对象封装了数据和行为,并提供了一组公共接口,通过这些接口与其他对象交互。在面向对象编程中,数据和函数同属于一个对象,数据的状态变化和函数的调用是相互关联的,对象也可以继承和扩展其他对象的行为。面向对象编程是现代编程语言的主要范式。
两种编程方式各有优缺点,选择何种方式取决于具体的编程任务和个人偏好。过程性编程通常更适合简单、线性的问题,它的代码结构简单、逻辑清晰,便于调试和维护;而面向对象编程则更适合复杂、分布式的问题,它的代码组织严密、高度封装,便于扩展和重用。
在实践中,许多编程语言和框架均采用了两种编程方式的结合,通过面向对象编程为过程性编程提供抽象和接口,从而实现更高效、更可靠的程序设计。例如,C++ 就是一门支持面向对象编程和过程性编程的语言,而Java、Python、C# 等语言都十分强调面向对象编程。
10.2、抽象和类
抽象是面向对象编程中非常重要的概念,它指的是将具体的事物抽象成一般性的、概念性的、抽象的概念或模型。在编程中,抽象通常使用类来实现。
类是一种数据类型,描述了具有相同属性和行为的对象集合。类用于定义对象的模板,包括属性(数据成员)和操作(成员函数)两部分。属性表示对象的数据,成员函数表示对象的行为。类中的属性和成员函数通常被称为类的成员。类是一种抽象数据类型,它不会被直接实例化,而是通过类的对象(类的实例)来使用。
类可以被继承和派生。继承是指派生类继承其基类的属性和行为。派生是指从一个已有的类中创建一个新的类,新的类称为派生类,它包含基类的所有成员,还可以有自己的成员。派生类可以重载基类的成员函数,也可以添加新的成员函数和属性。
抽象类是指至少有一个纯虚函数的类,它无法被直接实例化,只能用作其他类的基类。纯虚函数是一个没有函数体的虚函数,它需要子类实现具体的行为。抽象类是一种特殊的类,它只存在于继承关系中,将类的公共部分抽象出来,定义了一个更为通用的类,并为派生类提供了一个接口。
抽象和类是面向对象编程的核心概念,它们在实践中被广泛应用。通过抽象和类,程序员可以模拟真实世界中的对象和关系,将复杂的问题分解成可管理的模块,并减少了程序的复杂度和维护难度。
10.2.1、类型是什么
在计算机科学中,类型是数据的某种抽象描述,用于指定数据的内容和操作方式。类型是任何(编程)语言的核心构件之一,决定了数据在计算机中的存储方式和执行行为。
在编程语言中,变量、常量、函数和表达式等都具有某一种类型。类型可以分为基础类型和复合类型两种。基础类型是最基本的数据类型,如整型、浮点型、字符型、布尔型等,它们只包含一个值。而复合类型是指包含多个基础类型和复合类型的数据类型,如数组、结构体、枚举、类等。
类型可以帮助程序员理解数据的结构和意义,也可以通过类型检查来改善代码的正确性和可维护性。类型还可以在编译期进行类型推断和优化,有利于提高程序的性能和效率。
类型可以是静态类型或动态类型。静态类型指在编译期确定类型,例如 C++ 中的强类型和 Java、C# 中的静态类型。而动态类型指在运行时确定类型,例如 Python 中的动态类型。
类型也可以进行转换和强制转换,例如将整数转换成浮点数、将浮点数转换成整数。这种转换会产生数据类型丢失或精度损失的风险,需要谨慎使用。
10.2.2、C++中的类
C++中的类是一种用户自定义的类型,它可以包含数据成员、函数成员、构造函数和析构函数等内容,用于描述一类相似对象的属性和行为。类具有封装性、继承性和多态性的特点,是实现面向对象编程的基本单位。
C++中的类声明通常包括公共和私有两个部分。公共部分定义类的接口,包括公共函数和公共数据成员,用于与外界交互。私有部分定义类的实现细节,包括私有函数和私有数据成员,用于隐藏类的实现细节。
C++中的类可以具有构造函数和析构函数。构造函数用于创建对象并初始化对象成员,析构函数用于销毁对象并释放资源。C++的类支持带参数的构造函数、拷贝构造函数、移动构造函数、析构函数和运算符重载等特性,使得用户可以更加灵活地使用、创建和处理对象。
C++的类还支持继承和多态。继承是指派生类继承其基类的属性和函数,派生类可以重载基类函数、扩展新的函数成员、添加新的数据成员。多态是指同一种类型的对象在不同情况下表现出不同的行为,C++中通过虚函数和虚函数表实现多态性。
C++中的类是面向对象编程的重要组成部分,其提供了丰富的特性和强大的表现力,使其可以被广泛应用于各种应用场合中。
10.2.3、实现类成员函数
在C++中,类成员函数可以在类的内部声明和实现,也可以在类的外部进行实现。在类的内部声明成员函数,需要使用类的访问限定符号(public、private、protected)进行限定和修饰。
如果在类的内部仅声明而不实现函数成员,那么函数成员应该声明为纯虚函数,这样就可以定义一个抽象基类,让其派生类必须实现这个函数,从而实现多态性。
在类外部实现成员函数,需要使用作用域解析运算符“::”来指定函数所属的类,例如:
class MyClass {
public:
void myFunction(); // 在类内部声明函数
};
// 在类外部实现函数
void MyClass::myFunction() {
// 函数的具体实现
}
在实现成员函数的过程中,需要注意成员函数的访问限定符号应该与其声明时定义的相同。同时,也需要保证成员函数访问和操作的数据成员是所在类的成员。
成员函数还可以被定义为const函数,这样表示它不会对类的任何数据成员进行修改。在函数定义时,需要在函数后面添加const关键字进行修饰,例如:
class MyClass {
public:
int getValue() const; // const函数的声明
private:
int value_;
};
// 在类外部实现const函数
int MyClass::getValue() const {
return value_;
}
在实现类成员函数时,还需要注意一些细节,例如函数参数和返回值的类型、引用和指针的使用、异常处理等。同时,为了提高程序的可读性、可维护性和可扩展性,建议为每个类成员函数编写注释和文档,并遵循编程规范和最佳实践。
10.2.4、使用类
使用类的步骤通常包括以下几个方面:
-
定义类:定义一个类,该类包括成员变量和成员函数。成员变量用于描述对象的属性,成员函数用于描述对象的行为。
-
创建对象:使用类定义创建一个或多个对象,即类的实例。创建对象时,可以使用该类的构造函数进行初始化。
-
访问成员:使用对象名和成员的名称来访问对象的成员变量和成员函数。访问成员是通过.或->操作符实现的,其中点操作符用于访问对象的成员变量和非静态成员函数,箭头操作符用于访问对象指针的成员变量和非静态成员函数。
-
使用类的静态成员:访问类的静态成员时,使用类名和作用域解析运算符::来引用它们。 静态成员函数可以通过类的名称直接调用,而静态成员变量需要在使用前初始化。
-
继承:如果该类是被继承的,可以使用派生类对象来访问继承来的成员变量和成员函数。
-
析构对象:使用析构函数来销毁对象,释放其占用的内存和释放资源。当对象超出作用域,或者通过delete关键字释放对象时,会自动调用析构函数。
实际使用类时需要遵循面向对象编程的原则和最佳实践,例如尽可能使用封装、继承、多态等特性,避免使用全局变量和函数等不良编程习惯,同时要进行适当的异常处理和错误检查,以保证程序的正确性和健壮性。
10.3、类的构造函数和析构函数
构造函数和析构函数
构造函数是一种特殊的成员函数,用于创建和初始化对象。当创建新对象时,构造函数会自动执行,进行变量初始化、内存分配、资源申请等操作。在C++中,构造函数的名称与类名相同,没有返回值,可以重载。如果没有显式定义构造函数,编译器会提供一个默认的构造函数。在类的定义中,使用特殊的构造函数语法来定义构造函数,实例如下:
class MyClass {
public:
// 默认构造函数
MyClass() {
// 做一些初始化操作
}
// 常规构造函数,接受一个参数
MyClass(int value) {
// 做一些初始化操作
}
// 拷贝构造函数
MyClass(const MyClass& other) {
// 做一些初始化操作,复制对象
}
};
// 创建对象时,会自动调用相应的构造函数
MyClass obj1; // 默认构造函数
MyClass obj2(100); // 常规构造函数
MyClass obj3(obj1); // 拷贝构造函数
析构函数与构造函数相反,用于销毁对象并释放对象占用的资源。当对象的生命周期结束时,析构函数会自动执行,进行资源的回收、内存的释放等操作。在C++中,析构函数的名称也与类名相同,但以析构符“~”开头,在类的定义中,使用特殊的析构函数语法来定义析构函数,实例如下:
class MyClass {
public:
// 构造函数
MyClass() { }
// 析构函数
~MyClass() {
// 做一些清理操作,释放资源等
}
};
// 创建对象时,会自动调用构造函数和析构函数
MyClass obj; // 构造函数和析构函数
构造函数和默认参数
C++的构造函数支持默认参数。在定义构造函数时,可以像普通函数一样定义默认参数,这样在使用构造函数时,可以按需提供参数,也可以使用默认参数,实例如下:
class MyClass {
public:
// 默认构造函数,参数有默认值
MyClass(int x = 0, int y = 0) {
// 做一些初始化操作
}
};
// 创建对象时,可以使用默认参数
MyClass obj1; // 使用默认参数0和0
MyClass obj2(100); // 使用默认参数0
MyClass obj3(100, 200); // 不使用默认参数
当有多个构造函数时,需要注意默认参数的设置,避免构造函数冲突和调用不明确的问题。
构造函数和自动类型推导
C++11引入了auto关键字和类模板推导,可以让编译器自动推导变量和类型。在使用auto关键字创建对象时,可以使用构造函数进行自动类型推导,实例如下:
auto obj = MyClass(100); // 使用构造函数进行自动类型推导
这样,编译器会自动推导MyClass类型,并使用参数100调用对应的构造函数。需要注意,使用auto关键字声明变量时,必须初始化变量,否则编译器无法推导类型。
10.3.4、析构函数和动态内存分配
在使用动态内存分配时,需要手动释放内存,否则程序可能会出现内存泄漏等问题。在C++中,可以使用new和delete运算符实现动态内存分配和释放,也可以使用智能指针等RAII技术进行自动内存管理。
当类包含动态分配的资源时,需要在析构函数中释放这些资源,以避免内存泄漏。在析构函数中释放动态分配的资源时,应当使用与new运算符对应的delete运算符,确保释放的内存是动态分配的内存,而不是栈上分配的内存。
例如:
class MyClass {
public:
// 构造函数,动态分配内存
MyClass() {
p_ = new int[100];
}
// 析构函数,释放动态分配的内存
~MyClass() {
delete[] p_;
}
private:
int *p_;
};
在使用该类时,当对象超过作用域时,会自动调用析构函数释放动态分配的内存,避免内存泄漏。
需要注意的是,当类中包含指针成员时,需要正确地管理内存,避免内存泄漏和悬垂指针的问题。此时需要使用复制构造函数和赋值运算符等操作进行深拷贝和浅拷贝的处理。同时,也可以使用C++11引入的移动构造函数和移动赋值运算符,实现高效的资源管理和移动语义。
10.3.1、声明和定义构造函数
在C++中,声明和定义构造函数的方法与普通函数类似。在类的定义中,可以声明构造函数,也可以定义构造函数。声明和定义构造函数的语法如下:
声明构造函数:在类的定义中声明构造函数,不需要实现构造函数的功能,只起到告诉编译器该类有构造函数的作用:
class MyClass {
public:
MyClass(); // 声明构造函数
};
定义构造函数:在类外实现构造函数的功能,定义时需要使用类的名称和作用域解析运算符“::”来限定,例如:
MyClass::MyClass() {
// 实现构造函数的功能
}
还可以实现带参数的构造函数,方法与定义默认构造函数类似。例如,定义一个带有两个参数的构造函数:
class MyClass {
public:
MyClass(int x, int y); // 声明构造函数
};
MyClass::MyClass(int x, int y) {
// 实现构造函数的功能
}
需要注意的是,函数声明时不能包含函数体,函数定义时必须包含函数体。在定义构造函数时,应当完成类的初始化、内存分配、资源申请等操作,保证类的对象能够正确地进行使用。
如果类没有定义任何构造函数,编译器会提供一个默认的构造函数,也称为合成构造函数。默认构造函数不接受任何参数,仅负责初始化成员变量。在对象创建时,如果没有显式调用构造函数,编译器会自动调用默认构造函数对成员变量进行默认初始化。但如果类定义了至少一个构造函数,则编译器不会再提供默认构造函数,需要程序员自行实现。
因此,对于一个类而言,至少需要定义一个构造函数来创建和初始化对象。而对于不同的应用场景,可能需要定义多个不同的构造函数来满足不同的需求。
10.3.2、使用构造函数
在C++中,可以使用构造函数来创建类的对象,并在创建对象时完成初始化操作。下面介绍如何使用构造函数。
使用默认构造函数:如果类定义了默认构造函数,可以在创建对象时不传递任何参数,使用默认构造函数进行初始化。例如:
class MyClass {
public:
MyClass(); // 默认构造函数
};
MyClass obj; // 使用默认构造函数创建对象
使用带参数的构造函数:如果类定义了带参数的构造函数,可以在创建对象时传递相应的参数,使用带参数的构造函数进行初始化。例如:
class MyClass {
public:
MyClass(int x, int y); // 带参数的构造函数
};
MyClass obj(1, 2); // 使用带参数的构造函数创建对象
使用列表初始化:C++11标准引入了列表初始化语法,可以使用花括号{}来初始化对象,例如:
class MyClass {
public:
MyClass(int x, int y); // 带参数的构造函数
};
MyClass obj{1, 2}; // 使用列表初始化创建对象
在使用列表初始化时,会自动调用匹配的构造函数进行初始化。如果类没有定义任何构造函数,则会尝试进行聚合初始化或零值初始化。如果列表初始化的元素数量与类的成员数量不符,则会出现编译错误。
需要注意的是,如果未定义任何构造函数,则编译器会提供默认构造函数。如果定义了至少一个构造函数,则默认构造函数会被禁用。如果希望继续使用默认构造函数,需要手动定义一个不带参数的构造函数。例如:
class MyClass {
public:
MyClass(); // 手动定义默认构造函数
MyClass(int x, int y); // 带参数的构造函数
};
MyClass obj1; // 使用手动定义的默认构造函数创建对象
MyClass obj2(1, 2); // 使用带参数的构造函数创建对象
另外,如果类定义了多个构造函数,可以根据不同的参数类型和数量来选择使用哪个构造函数进行初始化。C++中的多态性可以实现多种具有相同名称但参数不同的构造函数存在的情况。
10.3.3、默认构造函数
默认构造函数是一个不带任何参数的构造函数,如果在类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。默认构造函数会对类中的所有成员变量进行默认初始化。
默认构造函数的作用:
-
对类中的成员变量进行默认初始化:默认构造函数会对成员变量进行默认初始化,不同的数据类型会有不同的默认值,例如数值类型会初始化为0,布尔类型会初始化为false,指针类型会初始化为nullptr等。
-
允许创建无参对象:当类中没有定义任何构造函数时,可以使用默认构造函数来创建无参对象。
-
允许派生类的默认构造函数调用基类的默认构造函数:当派生类没有定义任何构造函数时,编译器会自动生成一个默认构造函数,该默认构造函数会隐式调用基类的默认构造函数,在实现继承时非常有用。
默认构造函数的语法:
默认构造函数是一个不带任何参数的构造函数,其语法如下:
class MyClass {
public:
MyClass(); // 默认构造函数
};
如果已经定义了其他构造函数,可以使用=default
来显示声明默认构造函数。
class MyClass {
public:
MyClass(int x) {}; // 其他构造函数
MyClass() = default; // 显示声明默认构造函数
};
需要注意的是,如果类定义了至少一个构造函数,则编译器不会自动生成默认构造函数,如果想使用默认构造函数进行对象的默认初始化,则需要在类中手动定义一个不带任何参数的构造函数,例如:
class MyClass {
public:
MyClass(){} //手动定义默认构造函数
};
另外,有些情况下需要禁用默认构造函数,通过在类中声明private
修饰的不带参数的构造函数来实现禁用,默认构造函数会被删除,无法使用。
10.3.4、析构函数
析构函数(Destructor)是一种特殊的成员函数,在对象生命周期结束时自动被调用,用于清理对象所使用的资源。例如,当一个对象被销毁时,通过析构函数可以释放该对象所分配的内存、关闭文件或网络连接等资源。
析构函数的命名规则与构造函数相同,即以波浪号(~)加类名表示,在类的定义中只能有一个析构函数,不能有参数和返回值,其语法如下:
class MyClass {
public:
~MyClass(); // 析构函数
};
析构函数与构造函数的执行顺序正好相反。当一个对象被销毁时,会先自动调用析构函数,然后再释放对象所占用的内存空间。如果没有显式定义析构函数,编译器会自动生成一个空的析构函数,在删除对象时不会进行任何操作。
需要注意的是,如果类中定义了指针等动态分配的资源,需要手动编写析构函数以释放资源,以防止内存泄漏和资源占用。例如:
class MyClass {
public:
MyClass() {
data = new int[10]; // 在构造函数中分配内存
}
~MyClass() { // 需要手动释放内存
delete[] data;
}
private:
int* data;
};
在上述示例中,构造函数中使用new
运算符动态分配了一块大小为10的整数数组内存空间,如果不手动释放该空间,将会导致内存泄漏。因此,需要在析构函数中使用delete[]
运算符释放内存空间。
10.3.5、改进Stock类
在前面的例子中,我们定义了一个简单的Stock类,并实现了两个成员函数来设置和显示股票数据。在这个例子中,我们假设构造函数和析构函数都只是空的函数体,没有任何操作,因此可能会导致一些问题。
首先,如果Stock类创建的对象直接超出了其作用域,可能会导致资源泄漏问题。当我们创建Stock类对象时,系统首先会调用构造函数,当对象超出作用域时,系统会自动调用析构函数。如果我们的程序中存在一些需要分配或释放资源的操作,如动态内存分配、打开文件等,就需要在构造函数和析构函数中进行资源的分配和释放。
另外,如果没有正确地实现构造函数和析构函数,可能会导致对象的内存布局错误,从而影响程序的正确性和性能。
因此,我们需要在Stock类中正确地实现构造函数和析构函数,以确保对象的创建和销毁过程正确无误。
以下是基于前面的Stock类改进后的代码,主要在构造函数和析构函数中增加了一些资源的分配和释放操作,保证了对象的正确创建和析构。
#include <iostream>
#include <cstring>
class Stock
{
private:
char *company;
int len;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
Stock(); // 默认构造函数
Stock(const char *co, long n = 0, double pr = 0.0);
~Stock(); // 析构函数
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show() const;
const Stock &topval(const Stock &s) const;
};
Stock::Stock()
{
len = 0;
company = new char[len + 1];
company[0] = '\0';
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const char *co, long n, double pr)
{
len = std::strlen(co);
company = new char[len + 1];
std::strcpy(company, co);
shares = n;
share_val = pr;
set_tot();
}
Stock::~Stock()
{
delete[] company;
}
void Stock::buy(long num, double price)
{
shares += num;
share_val = price;
set_tot();
}
void Stock::sell(long num, double price)
{
shares -= num;
share_val = price;
set_tot();
}
void Stock::update(double price)
{
share_val = price;
set_tot();
}
void Stock::show() const
{
std::cout << "Company: " << company
<< " Shares: " << shares << '\n'
<< " Share Price: $" << share_val
<< " Total Worth: $" << total_val << '\n';
}
const Stock &Stock::topval(const Stock &s) const
{
if (s.total_val > total_val)
return s;
else
return *this;
}
在上述代码中,我们在Stock类中增加了默认构造函数和析构函数的定义,同时也保留了之前的带参数构造函数和其他成员函数。在默认构造函数中,我们将公司名称字符串长度初始化为0,并用new运算符分配了一个长度为1的字符数组,它中包含一个空字符。在带参数构造函数中,我们再次使用new运算符分配了内存,然后将传入构造函数的字符串复制到company指针指向的内存。在析构函数中,我们使用delete运算符释放了company指针所指向的内存。
当然,现代的C++建议使用智能指针(如C++11中引入的std::unique_ptr、std::shared_ptr等)来管理动态内存,以避免手动处理内存分配和释放所带来的风险。
10.3.6、构造和析构函数小结
构造函数和析构函数是类中非常重要的成员函数,它们分别负责对象的创建和销毁过程。构造函数会在对象被创建时被调用,用于初始化对象的成员变量;析构函数则在对象被销毁时调用,用于释放对象占用的资源。
构造函数有以下特点:
- 构造函数的函数名必须与类名相同,没有返回类型,可以有参数列表。
- 构造函数被用于初始化对象的成员变量,包括基本数据类型、对象或指针等。
- 如果没有定义构造函数,编译器将会默认生成一个无参构造函数,但是如果定义了有参构造函数,则需要一起定义无参构造函数。
- 可以定义多个构造函数,也可以通过函数默认参数来达到一个构造函数满足多种对象初始化需求的目的。
析构函数有以下特点:
- 析构函数的函数名同样是类名前加“~”,没有返回类型,没有参数。
- 析构函数主要用于清理对象所占用的资源,例如释放动态分配的内存、关闭文件、断开网络连接等。
- 对象的销毁顺序与它们的创建顺序相反,即先创建后销毁。
- 如果类中有指针类型成员变量,则需要在析构函数中手动释放内存,否则会出现内存泄漏问题。
需要注意的是,当类中涉及到动态内存分配时,需要重载赋值运算符和拷贝构造函数,以保证对象的正确复制和赋值。此外,C++11中还引入了移动构造函数和移动赋值运算符,用于优化对象的移动操作。
总之,构造函数和析构函数是C++面向对象编程中非常重要的一部分,能够帮助程序员管理对象的生命周期和资源。掌握构造函数和析构函数的基本知识,能够写出健壮且高效的类,并且避免常见的内存泄漏和效率问题。
10.4、this指针
在C++中,每个对象都有一个指向自己的指针,即this
指针。this
指针是一个隐式参数,它在每次调用非静态成员函数时自动被传递给该函数,用于指示该函数所作用的对象的地址。
this
指针的主要作用有:
1、避免歧义:当成员函数的参数名称与类的成员名称相同时,可以通过this
指针来访问成员变量,以避免歧义。例如:
class Person {
public:
void setName(string name) {
this->name = name;
}
private:
string name;
};
在setName
函数中,name
既可能是参数名,也可能是成员变量名,但是通过this
指针来访问成员变量就可以避免歧义。
2、在成员函数中返回对象本身:在成员函数中,可以通过this
指针返回对象本身,从而支持链式调用。例如:
class Person {
public:
Person& setName(string name) {
this->name_ = name;
return *this;
}
private:
string name_;
};
在上述例子中,setName
函数返回指向Person
对象的引用,这样可以实现链式调用,例如:
Person person;
person.setName("Tom").setAge(20).setGender("Male");
3、在构造函数和析构函数中使用:在构造函数和析构函数中,可以使用this
指针来操作当前对象的成员变量和方法。例如:
class Person {
public:
Person(string name, int age) : name_(name), age_(age){
this->init();
}
private:
string name_;
int age_;
void init() {
// 对象初始化代码
}
};
在上述例子中,init()
函数被构造函数调用时,使用this
指针来调用当前对象的成员方法。
总之,this
指针是C++面向对象编程中非常重要的一部分,能够帮助程序员操作当前对象的成员变量和方法,并且避免歧义问题。加深对this
指针的理解,对于写出健壮且高效的类具有重要意义。
10.5、对象数组
对象数组是由同一类的多个对象按照一定的顺序存储在连续的内存空间中的数组,它的特点是数组的每个元素都是一个对象。在C++中,可以使用类类型的构造函数来创建对象数组,并可以使用类类型的析构函数来销毁对象数组。
创建对象数组的语法如下:
ClassName objectArray[arraySize];
其中,ClassName
是类名,objectArray
是对象数组名,arraySize
是对象数组的长度,即对象的个数。
例如,假设有一个名为Person
的类,它具有name
和age
两个私有成员变量,那么可以通过以下方式创建对象数组:
Person personArray[3] = { Person("Tom", 20), Person("Jerry", 22), Person("Alice", 18) };
在上述例子中,创建了一个长度为3的Person
对象数组,其中每个元素都是一个Person
对象,并分别使用给定的姓名和年龄进行初始化。
可以通过下标索引来访问对象数组中的元素,例如:
cout << personArray[0].getName() << endl;
上述代码将输出对象数组中第一个元素的名称。
当对象数组不再被使用时,需要调用析构函数来销毁它,例如:
for (int i = 0; i < 3; i++) {
personArray[i].~Person();
}
其中,~Person()
是Person
类的析构函数,上述代码将按照相反的顺序,依次销毁对象数组中的每个元素。
需要注意的是,在对象数组中,每个对象都是独立的,它们的内存空间是连续的,但是它们的生命周期是相互独立的。当对象数组中的任何一个元素被销毁时,只会调用该元素对应的析构函数,不会影响其他元素的生命周期。
10.6、类作用域
在C++中,类作用域是指类定义内部的作用域。在类定义内部声明的变量、函数或类型,只在类内部可见,不能在类外部直接访问。这种限制被称为访问控制,可以通过访问控制符(public、private和protected)来实现。
举个例子,假设有一个名为Person
的类,它包含私有成员变量name
和age
、以及公有成员函数getName()
和getAge()
。在类定义中声明这些成员时,它们的作用域都被限定在类内部。
class Person {
private:
string name;
int age;
public:
string getName() const;
int getAge() const;
};
在上述代码中,成员函数getName()
和getAge()
被声明为公有的,可以在类外部访问。而成员变量name
和age
被声明为私有的,只能在类内部使用。
在类定义中还可以定义嵌套类型,这些类型的作用域也被限定在类内部。例如,假设在Person
类中定义了一个名为Address
的嵌套结构体,那么Address
只能在类内部和派生类中使用,不能在类外部直接访问。
class Person {
private:
string name;
int age;
struct Address {
string street;
string city;
string state;
string zipCode;
};
public:
string getName() const;
int getAge() const;
};
总之,在C++中,类作用域是一种非常有用的特性,它可以帮助程序员隐藏实现细节、封装数据和行为,并将它们组织在一起形成一个独立的单元。
10.7、抽象数据类型
抽象数据类型(ADT)是一种通过定义数据类型的组成部分和支持的操作来描述数据类型的一种方式。在C++语言中,类和结构体是实现ADT的主要方式之一。
ADT的主要特点有:
-
抽象性:ADT描述了一个数据类型的特征和操作,但是不涉及具体实现的细节。这样可以隐藏实现细节,使用户只关注数据类型本身的特性。
-
封装性:ADT支持对数据和操作的封装,使得将数据类型的内部实现细节与用户代码分离开来。这样可以保证数据的安全性和一致性。
-
继承性:ADT支持通过继承实现新的数据类型,这样可以在不改变原有数据类型的基础上扩展其功能,并且实现代码重用。
-
多态性:ADT允许运行时确定具体实例化的对象,这样可以在代码继承或组合时使代码更加灵活。
例如,假设我们要实现一个ADT用于描述分数,它包含一个分子和分母,以及支持计算两个分数之和的操作。可以使用类来实现这个ADT,代码如下:
class Fraction {
public:
Fraction(int numerator, int denominator) : numerator_(numerator), denominator_(denominator) {}
Fraction operator+(const Fraction& other) const {
Fraction sum(0, 0);
sum.numerator_ = numerator_ * other.denominator_ + denominator_ * other.numerator_;
sum.denominator_ = denominator_ * other.denominator_;
return sum;
}
private:
int numerator_;
int denominator_;
};
在上述代码中,Fraction
类包含私有成员变量numerator_
和denominator_
,用于存储分数的分子和分母。它还定义了一个公有成员函数operator+()
,用于计算两个分数之和,并返回一个新的Fraction
对象。
通过上述代码,我们定义了一个抽象数据类型Fraction
,它封装了分数的数据和操作,并提供了较好的抽象性,使用户可以更加便捷地处理分数的运算。
总之,抽象数据类型是一种非常重要的概念,在C++中可以通过类和结构体来实现,它们是面向对象编程的重要组成部分,可以提高程序的可维护性、可扩展性和代码的重用性。