课件
8.1 抽象与封装
抽象
定义与声明
访问控制
类的实现与使用
类的实现与引用例子
指针
notes
简述
- 8.1.1 抽象
- 理解“抽象”的含义,了解面向对象开发的流程
- 8.1.2 定义与声明
- 了解定义和使用类的基本流程
- 8.1.3 访问控制
- 理解访问控制的目的是为了实现类内部的数据封装
- 认识 3 个访问修饰符 public、protected、private
- 8.1.4 类的实现与使用
- 理解课件中提到的 Clock 类的 demo 源码,知道如何使用流控制符 setw 和 setfill 实现打印时间,当时间不足 2 位的时候,前面自动补零
- 理解成员方法在类内实现、类外实现的差异
- 掌握两种访问对象成员的方式
- 熟悉定义和使用类的基本流程(同 8.1.2)
- 本节介绍的“类的实现”,主要是介绍“类的成员方法的实现”
- 8.1.5 类的实现与使用例子
- 实现一个简单的 int 数组类(非常棒的练手示例)
- 8.1.6 指针
- 对象指针
- 对象引用
- this 指针
抽象
- 抽象是 将某个事物从具体的个体中抽取出来,把它独立出来,形成一个概念或者模型
- 抽象是 对具体对象(问题)进行概括,提炼出这一类的公共性质并加以描述的过程
- 抽象是 建立在某个具体的事物基础上的,而不是独立存在的
- 在计算机编程中,抽象指的是从某个具体的实现中提取出一些基本特征和行为,将其定义为抽象类或接口,作为代码复用的基础,从而提高代码的可维护性和扩展性
- 在面向对象编程中,抽象是实现多态的重要手段之一,通过抽象类或接口来实现多态性,使得程序更加灵活、可扩展和易于维护
抽象是相对的,而非绝对的
- 抽象并不是一个绝对的概念,而是相对的,具有一定的主观性和相对性
- 抽象是通过对事物特征的提炼和概括得出的,所以 抽象的结果可以因人而异,也可以因时间和空间等条件的不同而异
- 在研究具体问题时,侧重点不同,可能会产生不同的抽象结果
- 解决同一问题时,要求不同,也可能会产生不同的抽象结果
进行面向对象开发时的一般流程
从抽象方法出发,通过封装实现具体功能,最终通过类来实现面向对象的编程
- 抽象方法:抽象方法是指定义一个方法,该方法只有方法名、参数、返回类型,而没有具体的实现,其实现由子类来完成。这个过程可以看作是从抽象到具体的过程,即先定义一个抽象的框架,然后再填充具体实现细节。
- 封装:将数据和方法包含在一个单元中,防止外部直接访问和修改内部的数据,通过定义接口和实现来实现数据的访问和修改,从而保证了数据的安全性和可维护性。
- 类:封装和抽象的具体实现,是对具有相同属性和操作的对象的抽象,通过定义类可以实现数据和方法的封装,同时可以利用继承和多态等特性实现抽象的复用和灵活的扩展。
抽象方法、封装和类是面向对象编程的核心概念,也是实现面向对象编程的重要手段。
抽象的其它含义
C++ 中的“抽象”也指:
- 抽象类
- 纯虚函数
抽象类:
- 抽象类是一种特殊的类,它不能被实例化,只能被继承
- 抽象类通常用于描述一种抽象的概念或行为,它只定义接口,而不提供具体的实现
- 具体的实现由其子类来完成
纯虚函数:
- 抽象类中包含至少一个纯虚函数,纯虚函数是没有实现的虚函数,其定义为“virtual 返回类型 函数名() = 0;”
- 纯虚函数用于规范接口,并强制其子类必须实现该函数,从而实现多态性的特性
- C++ 中的“抽象”是一种把具体的事物抽象出来,从而实现代码复用和扩展的一种机制
定义和使用类的基本流程
#include <iostream>
using namespace std;
// 定义类
class Person {
private:
std::string name;
int age;
public:
Person(std::string n, int a) {
name = n;
age = a;
}
void print() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
// 实例化:使用定义的 Person 类定义一个 Person 实例
Person p("Tom", 18);
// 使用 Person 实例
p.print(); // => Name: Tom, Age: 18
return 0;
}
访问控制
- C++ 中的访问控制修饰符是用来控制类成员的访问权限的
- 访问控制修饰符对于类的封装性和继承性有着重要的作用
- C++ 中的访问控制修饰符(Access Control Modifier):public、private、protected
- 访问控制修饰符的默认值:如果类成员没有指定访问控制修饰符,则默认为 private
- 访问修饰符的作用:
- 可以帮助开发者控制类成员的访问权限,从而保护类的内部实现,减少代码耦合度,提高代码的可维护性和安全性
- 在类的外部,不能直接访问私有成员和保护成员,可以通过公共成员函数来访问它们。这也是封装的一种体现,保护了类的内部实现细节,同时提供了对外的公共接口
- 数据封装的目的就是信息隐蔽
- 将数据和行为组合到一个单元中,并限制外部对数据的直接访问,而通过类中定义的公有接口进行访问和操作,从而保证了数据的安全性和一致性
- 为了达到信息隐蔽,在 C++ 类中,并非所有的成员都是对外可见的(或者说,是类外可以访问的)
C++ 中的访问控制修饰符(Access Control Modifier):
- public
- 用于指定类的公有成员,可以被类的对象和外部函数访问
- 在关键字 public 后面声明,它们是类与外部的接口,任何类内、类外函数都可以访问公有数据和函数
- private
- 用于指定类的私有成员,只能在类的内部被访问
- 在关键字 private 后面声明,只允许本类中的函数访问,而类外的任何函数都不能访问
- protected
- 用于指定类的保护成员,只能在类的内部和子类中被访问
- 在关键字 protected 后面声明的数据成员或成员函数。与 private 类似,其差别表现在继承与派生时对派生类的影响不同(在第 9 章中再描述)
class ClassName {
public:
公共成员
protected:
保护成员
private:
私有成员
}
- 公有成员:可以在类的内部和外部通过对象名和指向对象的指针访问
- 私有成员:不能被类的对象和外部函数访问,只能通过公有成员函数来访问
- 保护成员:不能被类的对象和外部函数访问,只能通过公有成员函数和子类对象来访问
一些注意事项:
- 在类内,访问修饰符可以出现多次,并非一个访问修饰符只能出现一次
- 访问修饰符的书写顺序没有强制要求,并非 public 必须写在最前面
- 同一个成员,只能被一个访问修饰符修饰
类内、类外
- 类内:
- 类的定义部分
- 包括类名、成员函数、数据成员等……
- 在类声明之内
- 类外:
- 类的实现部分
- 在类定义外实现的成员函数和数据成员
- 在类声明之外
public、protected、private
#include <iostream>
using namespace std;
class MyClass {
public:
int x;
protected:
int y;
private:
int z;
public:
MyClass() {
x = 1;
y = 2;
z = 3;
}
void printValues() {
cout << "x = " << x << endl;
cout << "y = " << y << endl;
cout << "z = " << z << endl;
}
};
int main() {
MyClass obj;
obj.printValues();
cout << "obj.x = " << obj.x << endl; // public 成员可以在类外访问
// cout << "obj.y = " << obj.y << endl; // protected 成员不能在类外访问
// cout << "obj.z = " << obj.z << endl; // private 成员不能在类外访问
return 0;
}
/* 运行结果:
x = 1
y = 2
z = 3
obj.x = 1
*/
类
- 类是一种数据结构
- 类的变量称作“类的实例”,或“类的对象”
- 在面向对象编程中,类是用于创建对象的蓝图或模板,它描述了一组数据属性和方法操作这些属性的行为
- 类通常包括构造函数、析构函数、成员函数、静态成员、常量和枚举类型等
- 类可以看做是一个自定义的数据类型,它可以包含变量和函数,并且可以被用来实例化多个对象
- 类的使用可以使程序的设计更加模块化,易于维护和扩展
- 类的实例化即创建了一个具体的对象,该对象可以调用类中定义的方法,访问类中定义的属性
- 定义对象(类实例化)的语法:
类名 对象名;
- 例:
Clock clock;
- 其中 Clock 是时钟类的类名
- clock 是这个类的变量
- 例:
- 访问类成员的方式:通过对象访问、通过对象指针访问
访问类成员的方式
通过对象访问类成员:
对象名.公有成员函数名(形参列表)
对象名.公有数据成员
通过对象指针访问类成员:
对象的指针 -> 公有成员函数名(形参列表)
对象的指针 -> 公有数据成员
类的实现
#include <iostream>
#include <iomanip>
using namespace std;
class Clock {
public:
void SetTime(int h, int m, int s) { // 在类内实现成员函数,编译器按内联函数处理
hour = h;
minute = m;
second = s;
}
void ShowTime() const {
cout << setfill('0') << setw(2) << hour << ":"
<< setfill('0') << setw(2) << minute << ":"
<< setfill('0') << setw(2) << second << endl;
}
private:
int hour;
int minute;
int second;
};
int main() {
Clock clock;
clock.SetTime(8, 14, 30); // => 08:14:30
clock.ShowTime();
return 0;
}
流控制符 setw 和 setfill:
- setw(n) 表示输出的下一个值占 n 列
- setfill(c) 表示用字符 c 填充 setw() 指定的列
#include <iomanip>
#include <iostream>
using namespace std;
class Clock {
public:
void SetTime(int h, int m, int s);
void ShowTime();
private:
int hour;
int minute;
int second;
};
void Clock::SetTime(int h, int m, int s) { // 可以在类外实现成员函数
hour = h;
minute = m;
second = s;
}
void Clock::ShowTime() {
cout << setfill('0') << setw(2) << hour << ":" << setfill('0') << setw(2)
<< minute << ":" << setfill('0') << setw(2) << second << endl;
}
int main() {
Clock clock;
clock.SetTime(8, 14, 30); // => 08:14:30
clock.ShowTime();
return 0;
}
对比两种写法之间的差异:
- 是否是内联函数
- 在类内实现成员函数时,编译器会将其视为内联函数处理,即在编译时将函数代码嵌入到每个调用点中,以提高执行效率
- 在类外实现成员函数时,则不一定会被视为内联函数
- 是否可以访问类内成员
- 在类内实现成员函数时,可以直接访问类的私有成员变量
- 在类外实现成员函数时,需要使用类的访问控制修饰符(public、protected、private)来控制对私有成员的访问
- 是否需要作用域修饰符
::
#include <iomanip>
#include <iostream>
using namespace std;
class Clock {
public:
void SetTime(int h, int m, int s);
void addHour(int h);
void addMinute(int m);
void addSecond(int s);
void ShowTime();
private:
int hour;
int minute;
int second;
};
void Clock::SetTime(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
void Clock::addHour(int h) { hour += h; }
void Clock::addMinute(int m) { minute += m; }
void Clock::addSecond(int s) { second += s; }
void Clock::ShowTime() {
cout << setfill('0') << setw(2) << hour << ":" << setfill('0') << setw(2)
<< minute << ":" << setfill('0') << setw(2) << second << endl;
}
int main() {
Clock clock_1, clock_2;
clock_1.SetTime(9, 5, 25);
clock_2.SetTime(15, 16, 45);
cout << "clock_1 time: ";
clock_1.ShowTime();
cout << "clock_2 time: ";
clock_2.ShowTime();
clock_1.addHour(3);
clock_2.addMinute(8);
cout << "clock_1 time: ";
clock_1.ShowTime();
cout << "clock_2 time: ";
clock_2.ShowTime();
cout << endl;
return 0;
}
/* 运行结果:
clock_1 time: 09:05:25
clock_2 time: 15:16:45
clock_1 time: 12:05:25
clock_2 time: 15:24:45
*/
每个对象各自包含了类中定义的各个数据成员的存储空间,但它们共享类中定义的成员函数。
问:语句 clock_1.hour = 21;
是否正确?
错,因为 hour 是 private 修饰的,是一个私有成员,只能在类中被访问,无法在类外访问。
问:如果将 ShowTime
修饰为 private 属性,还能通过 clock_2.ShowTime();
这种写法调用吗?
不行,因为被 private 修饰的成员,是一个私有成员,只能在类中被访问,无法在类外访问。
使用指针访问对象 8.1.4
会
因为指向的是同一块内存
#include <iomanip>
#include <iostream>
using namespace std;
class BaseClass {
int a;
public:
void set(int b) { a = b; }
void show() { cout << "a = " << a << endl; }
};
int main() {
BaseClass* c1 = new BaseClass();
BaseClass* c2 = NULL;
c1->set(5);
c1->show(); // => a = 5
c2 = c1;
c1->show(); // => a = 5
c2->show(); // => a = 5
c2->set(10);
c1->show(); // => a = 10
c2->show(); // => a = 10
return 0;
}
总结:定义和使用类类型的过程
实现一个简单的 int 数组类 8.1.5
第一步:抽象
- 数据抽象
- 数组大小
- 数组所占据的内存区
- 行为抽象
- 能够获取数组的大小
- 能够将数据保存到数组中
- 能够获取数组中保存的最大值
- 能够获取数组中保存的最小值
#include <iomanip>
#include <iostream>
using namespace std;
// 第二步:定义类
class IntArray {
int* data;
int size;
public:
void setArray(int len, int* in);
int getSize();
int setVal(int pos, int val);
int getMaxVal();
int getMinVal();
private:
int getVal(int condition);
};
// 第三步:实现类
void IntArray::setArray(int len, int* in) {
size = len;
data = new int(len);
for (int i = 0; i < len; i++)
data[i] = in[i];
}
int IntArray::getSize() { return size; }
int IntArray::setVal(int pos, int val) {
if (pos < 0 || pos > size)
return -1;
data[pos] = val;
return 0;
}
int IntArray::getVal(int condition) {
int temp = data[0];
for (int i = 1; i < size; i++) {
if (condition == 0) { // condition 为 0,找最大
if (data[i] > temp)
temp = data[i];
} else { // condition 不为 0,找最小
if (data[i] < temp)
temp = data[i];
}
}
return temp;
}
int IntArray::getMaxVal() { return getVal(0); }
int IntArray::getMinVal() { return getVal(1); }
int main() {
// 第四步:使用类
IntArray array;
int i, size, v, max, min, in[5] = {0};
array.setArray(sizeof(in) / sizeof(*in), in);
size = array.getSize();
cout << "please input " << size << " numbers:" << endl;
for (i = 0; i < size; i++) {
cin >> v;
array.setVal(i, v);
}
max = array.getMaxVal();
min = array.getMinVal();
cout << "max value is: " << max << endl;
cout << "min value is: " << min << endl;
return 0;
}
/* 运行结果:
please input 5 numbers:
93
29
03
45
87
max value is: 93
min value is: 3
*/
类和对象之间的关联 8.1.5
对象指针、对象引用 8.1.6
this 指针
- this 是指针
- this 的值不能被改变,总是指向当前调用的对象
- C++ 为每个非静态成员函数提供一个 this 指针
- this 指针是一个隐含的指针,它指向了正在被成员函数操作的那个对象
- this 指针不能显式声明,它只是非静态成员函数的一个形参
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person(string name, int age) {
this->name = name;
this->age = age;
}
void print() {
cout << "Name: " << this->name << endl;
cout << "Age: " << this->age << endl;
}
private:
string name;
int age;
};
int main() {
Person p("Tdahuyou", 24);
p.print();
return 0;
}
/* 运行结果:
Name: Tdahuyou
Age: 24
*/
编译器做了特殊处理:
- 隐含加上了名为 this 的形参:
Person(string name, int age)
相当于Person(Person *this, string name, int age)
void print()
相当于void print(Person *this)
- 隐含加上了名为 this 的形参:
Person p("Tdahuyou", 24);
相当于Person p(&p, "Tdahuyou", 24);
p.print();
相当于print(&p);
8.2 初始化与结束处理(构造函数与析构函数)
构造函数
析构函数
拷贝构造函数
notes
简述
构造函数、析构函数、拷贝构造函数
构造函数
- 构造函数是一种特殊的成员函数,本质上也是类的成员函数
- 构造函数用于在对象被创建时初始化对象的成员变量,解决数据成员的自动初始化问题
- 在对象被创建时,编译器会自动调用构造函数,以初始化对象的成员变量
- 构造函数的名称与类的名称相同,没有返回值类型(甚至没有 void)
- 如果没有显式地定义构造函数,编译器会自动生成默认构造函数,并使用默认构造函数进行初始化
- 如果显式地定义了构造函数,则必须在其中对所有成员变量进行初始化
- 构造函数可以进行参数传递,以便在创建对象时提供参数
- 可以在构造函数中初始化对象的成员变量,也可以在对象创建后通过成员函数进行初始化
- 构造函数可以被继承,派生类可以使用基类的构造函数
- 构造函数可以进行重载:在类定义中,可以定义多个构造函数,以便为不同的场景提供不同的初始化方法
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person() {
name = "Unknown";
age = 0;
}
Person(string n, int a) {
name = n;
age = a;
}
void Print() { cout << "Name: " << name << ", Age: " << age << endl; }
private:
string name;
int age;
};
int main() {
Person p1; // 调用默认构造函数
p1.Print();
Person p2("Tdahuyou", 24); // 调用带参数的构造函数
p2.Print();
return 0;
}
/* 运行结果:
Name: Unknown, Age: 0
Name: Tdahuyou, Age: 24
*/
Person 类有两个构造函数
- 无参数的默认构造函数:如果在创建对象时没有提供参数,则会使用默认构造函数
- 带参数的构造函数
默认的构造函数
- 如果没有显式地定义构造函数,C++ 编译器会自动生成默认构造函数,并使用默认构造函数进行初始化
- 默认构造函数是 不带参数的构造函数,它将对象的成员变量初始化为默认值
- 数字类型初始化为 0
- 布尔类型初始化为 false
- 指针类型初始化为 nullptr
- 等……
- 如果我们需要对成员变量进行初始化,可以定义自己的构造函数
- 如果定义了至少一个构造函数,则编译器不再生成默认构造函数
- 如果我们想要一个默认构造函数,并且也定义了其他构造函数,那么我们需要自己显式地定义一个不带参数的构造函数
#include <iostream>
using namespace std;
class Person {
public:
void SetAge(int age) { m_age = age; }
int GetAge() const { return m_age; }
private:
int m_age;
};
int main() {
Person p1, p2; // 不会走默认构造函数的逻辑
Person* p3 = new Person(); // 走默认构造函数逻辑,m_age 默认被初始化为 0
p1.SetAge(18);
cout << "age: " << p1.GetAge() << endl; // => age: 18
cout << "age: " << p2.GetAge() << endl; // => age: 793100416
cout << "age: " << (*p3).GetAge() << endl; // => age: 0
return 0;
}
由于 p2 对象没有被初始化,所以 m_age 成员变量的值是未定义的(不确定的),可能是任何值,因此 p2.GetAge() 的结果是不确定的。
实现构造函数的多种写法
class Person {
public:
Person() {
name = "";
age = 0;
}
private:
string name;
int age;
};
class Person {
public:
Person(string n, int a) {
name = n;
age = a;
}
private:
string name;
int age;
};
class Person {
public:
Person(const Person& p) {
name = p.name;
age = p.age;
}
private:
string name;
int age;
};
// 用于在创建一个新对象时将旧对象的数据复制到新对象中
class Person {
public:
Person() { Person("Anonymous", 0); }
Person(string n, int a) {
name = n;
age = a;
}
private:
string name;
int age;
};
class Person {
public:
Person(string name, int age) : m_name(name), m_age(age) {}
private:
string m_name;
int m_age;
};
Person(string name, int age) : m_name(name), m_age(age) {}
- Person 类的构造函数使用了成员初始化列表,将参数 name 和 age 分别赋值给成员变量 m_name 和 m_age
- 这种方式称为“成员初始化列表”(member initialization list)
- 成员初始化列表是构造函数头部的一部分,使用冒号分隔
- 在冒号后面,按照成员变量的声明顺序列出变量名和用于初始化它们的表达式,以逗号分隔
- 成员初始化列表可以初始化常量成员和引用成员,而构造函数函数体内不能对它们进行初始化
- 成员初始化列表的优点是能够提高代码的效率,避免了先默认初始化再赋值的过程
- C++11 之后的标准可用
传给构造函数实参的两种方式 8.2.1
#include <iostream>
using namespace std;
class Person {
public:
Person(int age) : m_age(age) {} // 相当于 Person(int age) { m_age = age; }
private:
int m_age;
public:
void print() { cout << "age: " << m_age << endl; }
};
int main() {
Person p1(18); // 相当于 Person p1 = 18;
p1.print(); // => age: 18
return 0;
}
具有缺省参数的构造函数 8.2.1
#include <iostream>
using namespace std;
class Person {
public:
Person(const std::string& name, int age = 18) {
m_name = name;
m_age = age;
}
void showInfo() const {
std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
}
private:
std::string m_name;
int m_age;
};
int main() {
Person p1("Alice"); // age 使用默认值 18
Person p2("Tdahuyou", 24); // age 使用 24 不使用默认值
p1.showInfo(); // => Name: Alice, Age: 18
p2.showInfo(); // => Name: Tdahuyou, Age: 24
return 0;
}
给 IntArray 添加构造函数 8.2.1
引用:
- 8.1 抽象与封装 在这节课中的 IntArray 的练习 demo 上进一步扩展
#include <iomanip>
#include <iostream>
using namespace std;
// 第二步:定义类
class IntArray {
int* data;
int size;
private:
void setArray(int len, int* in);
public:
IntArray(int len, int* in);
int getSize();
int setVal(int pos, int val);
int getMaxVal();
int getMinVal();
private:
int getVal(int condition);
};
// 第三步:实现类
IntArray::IntArray(int len, int* in) { setArray(len, in); }
void IntArray::setArray(int len, int* in) {
size = len;
data = new int(len);
for (int i = 0; i < len; i++)
data[i] = in[i];
}
int IntArray::getSize() { return size; }
int IntArray::setVal(int pos, int val) {
if (pos < 0 || pos > size)
return -1;
data[pos] = val;
return 0;
}
int IntArray::getVal(int condition) {
int temp = data[0];
for (int i = 1; i < size; i++) {
if (condition == 0) { // condition 为 0,找最大
if (data[i] > temp)
temp = data[i];
} else { // condition 不为 0,找最小
if (data[i] < temp)
temp = data[i];
}
}
return temp;
}
int IntArray::getMaxVal() { return getVal(0); }
int IntArray::getMinVal() { return getVal(1); }
int main() {
// 第四步:使用类
int i, size, v, max, min, in[5] = {0};
IntArray array(sizeof(in) / sizeof(*in), in);
size = array.getSize();
cout << "please input " << size << " numbers:" << endl;
for (i = 0; i < size; i++) {
cin >> v;
array.setVal(i, v);
}
max = array.getMaxVal();
min = array.getMinVal();
cout << "max value is: " << max << endl;
cout << "min value is: " << min << endl;
return 0;
}
/* 运行结果:
please input 5 numbers:
39
98
87
68
49
max value is: 98
min value is: 39 */
析构函数
- 析构函数是一种特殊类型的成员函数,用于在对象销毁时执行清理操作
- 自动调用:当对象的生命周期结束时(例如当对象超出其作用域、从函数返回、或delete操作符被用于对象时),析构函数被 自动调用
- 一个类中的析构函数可以用于释放由构造函数分配的资源,如动态分配的内存、打开的文件、数据库连接等。在对象销毁时,析构函数会自动被调用,以便清理这些资源
- 析构函数的命名规则与构造函数相同,以波浪号(~)开头,后面跟类名
- 与构造函数的不同点:
- 析构函数不接受任何参数,也没有返回值
- 一个类只能有一个析构函数,它不能被重载,而且不能被继承
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() { cout << "构造函数被调用" << endl; }
~MyClass() { cout << "析构函数被调用" << endl; }
};
int main() {
MyClass obj; // 创建一个 MyClass 对象
return 0;
} // obj 超出作用域,自动调用析构函数
/* 运行结果:
构造函数被调用
析构函数被调用 */
在上面的示例中,当 main 函数执行结束并退出时,obj 超出了它的作用域,因此系统自动调用了 MyClass 类的析构函数。
从输出结果可以看出,构造函数在创建 MyClass 对象时被调用,而析构函数在对象超出作用域时被调用,用于清理对象所占用的资源。
CString
- CString 是 MFC(Microsoft Foundation Classes)中的一个字符串类,用于在 Windows 程序中操作字符串
- CString 的实现中使用了动态内存分配,所以需要用到析构函数来释放内存
#include <cstring>
#include <iostream>
using namespace std;
class CString {
private:
int len;
char* buf;
public:
CString(int n);
void copy(const char* src);
void print();
};
CString::CString(int n) {
len = n;
buf = new char[n];
}
void CString::copy(const char* src) { strcpy(buf, src); }
void CString::print() { cout << buf << endl; }
void func() {
CString obj(64);
obj.copy("Hello World");
obj.print(); // => Hello World
}
int main() {
func(); // 问题:此时 obj 的 buf 所指向的内存空间没有释放
return 0;
}
#include <cstring>
#include <iostream>
using namespace std;
class CString {
private:
int len;
char* buf;
public:
CString(int n);
~CString();
void copy(const char* src);
void print();
};
CString::CString(int n) {
len = n;
buf = new char[n];
}
CString::~CString() {
delete[] buf;
cout << "释放 buf" << endl;
}
void CString::copy(const char* src) {
strcpy(buf, src);
cout << "给 buf 赋值为:" << src << endl;
}
void CString::print() { cout << "buf 的值为:" << buf << endl; }
void func() {
CString obj(64);
obj.copy("Hello World");
obj.print();
}
int main() {
func();
return 0;
}
/* 运行结果:
给 buf 赋值为:Hello World
buf 的值为:Hello World
释放 buf */
当 obj 对象被释放时,析构函数被自动调用,buf 所指向的内存空间被释放!
#include <cstring>
#include <iostream>
using namespace std;
class CString {
public:
CString(const char* str = nullptr) {
if (str != nullptr) {
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
} else {
m_data = new char[1];
*m_data = '\0';
}
}
CString(const CString& other) {
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
~CString() {
if (m_data != nullptr) {
delete[] m_data;
m_data = nullptr;
}
}
// 定义 CString 类的赋值运算符重载
CString& operator=(const CString& other) {
if (this == &other)
return *this;
delete[] m_data;
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;
}
// 定义 CString 类的输出运算符重载
friend ostream& operator<<(ostream& os, const CString& str) {
os << str.m_data;
return os;
}
private:
char* m_data;
};
int main() {
CString str1("Hello");
CString str2("World");
CString str3 = str1;
cout << str1 << endl; // Hello
cout << str2 << endl; // World
cout << str3 << endl; // Hello
return 0;
}
CString& operator=(const CString& other) {
if (this == &other)
return *this;
delete[] m_data;
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
return *this;
}
该函数的作用是实现 CString 类型变量之间的赋值操作
- 该函数首先进行自赋值检查,即判断目标对象是否与赋值对象是同一个对象,若是,则直接返回自身引用
- 接着,函数通过 delete[] 删除当前对象的 m_data 指向的内存空间,再通过 new char[] 重新分配内存空间,大小为被赋值对象的 m_data 的长度加 1,最后将被赋值对象的 m_data 内容拷贝到当前对象的 m_data 中。最后,函数返回自身引用。
friend ostream& operator<<(ostream& os, const CString& str) {
os << str.m_data;
return os;
}
该函数的作用是实现输出 CString 类型对象时的行为
- 在函数体内,使用重载的 << 运算符输出 CString 对象的 m_data 成员即可
- 该函数返回一个 ostream 类型的引用,使其可以实现多个 CString 对象连续输出
有关运算符重载的更多内容,见 11.2 运算符重载
拷贝构造函数
- 如果将与自己同类的对象的引用作为参数时,该构造函数就称为拷贝构造函数
- 拷贝构造函数将一个已经创建好的对象作为参数,根据需要将该对象中的数据成员逐一对应地赋值给新对象
- 拷贝构造函数起作用的地方:
- 构建新对象
- 对象作为函数参数
- 函数返回值
默认的拷贝构造函数
- 如果我们没有显式地定义拷贝构造函数,那么编译器会自动生成一个默认的拷贝构造函数
- 默认的拷贝构造函数会 对所有非静态成员进行拷贝,这通常是逐个成员变量进行复制
- 如果有指针成员变量,拷贝后的指针变量指向的是同一个地址,可能会导致不可预料的错误(比如同一块内存空间被释放多次)
- 在涉及指针成员变量的类中,我们需要显式地定义拷贝构造函数,以确保拷贝后的对象中的指针成员变量指向不同的地址
拷贝构造函数
#include <cstring>
#include <iostream>
using namespace std;
class Person {
public:
Person(const char* name, int age) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
m_age = age;
}
// 拷贝构造函数
Person(const Person& other) {
m_name = new char[strlen(other.m_name) + 1];
strcpy(m_name, other.m_name);
m_age = other.m_age;
}
~Person() { delete[] m_name; }
const char* GetName() { return m_name; }
int GetAge() const { return m_age; }
void SetName(const char* name) { strcpy(m_name, name); }
void SetAge(int age) { m_age = age; }
private:
char* m_name;
int m_age;
};
int main() {
Person p1("Tdahuyou", 24);
Person p2 = p1; // 调用拷贝构造函数
cout << p1.GetName() << endl; // => Tdahuyou
cout << p1.GetAge() << endl; // => 24
cout << p2.GetName() << endl; // => Tdahuyou
cout << p2.GetAge() << endl; // => 24
// p1、p2 是两个独立的对象,存放在两块不同的地址中
p1.SetAge(12);
cout << p1.GetAge() << endl; // => 12
cout << p2.GetAge() << endl; // => 24
return 0;
}
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v) { value = v; }
void add(int v) { value += v; }
void show() { cout << value << endl; }
};
Counter func(Counter obj) {
obj.add(6);
return obj;
}
int main () {
Counter b1 = 5;
Counter b2 = func(b1);
b1.show(); // => 5
b2.show(); // => 11
return 0;
}
8.3 指针,参数,静态,常,友元与组合等概念
类类型作为函数参数
对象数组
静态成员
常对象与常成员
友元
组合类
notes
类类型作为函数参数
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v) { value = v; }
void add(int v) { value += v; }
void show() { cout << value << endl; }
};
Counter func(Counter obj) {
obj.add(6);
return obj;
}
int main() {
Counter b1 = 5;
Counter b2 = func(b1);
b1.show(); // => 5
b2.show(); // => 11
return 0;
}
对象本身作为函数参数
由于 C++ 采用传值的方式传递参数,因此使用对象本身参数时,形参是实参的一个拷贝。在这种情况下,最好显式地为类定义一个拷贝构造函数,以免出现不容易发现的错误。
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v) { value = v; }
void add(int v) { value += v; }
void show() { cout << value << endl; }
};
void func(Counter& obj) { obj.add(6); }
int main() {
Counter b1 = 5;
b1.show(); // => 5
func(b1);
b1.show(); // => 11
return 0;
}
对象引用作为函数参数
这是一种推荐的方式。它比对象本身参数或对象指针参数都要容易理解和使用,同时没有任何的副作用。
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v) { value = v; }
void add(int v) { value += v; }
void show() { cout << value << endl; }
};
void func(Counter* obj) { obj->add(6); }
int main() {
Counter b1 = 5;
b1.show(); // => 5
func(&b1);
b1.show(); // => 11
return 0;
}
对象指针作为参数
对象指针指向实参对象,通过间接方式访问和修改它所指向的对象,实际上就是访问和修改实参对象。
对象数组
与任何其它数据类型一样,可以创建一个类的对象数组
例如:Clock clocks[10];
通过下标访问数组中的对象,进而访问该对象的公有成员
例如:clocks[3].ShowTime();
数组初始化:
- 显式初始化数组元素
const unsigned arr_size = 3;
int ia[array_size] = { 0, 1, 2 };
- 隐式初始化,若无显式初始化,则:
- 函数体外定义的内置数组,元素均为 0
- 函数体内定义的内置数组,元素无初始化
- 若元素为类类型,无论在那里定义,则自动调用该类的默认构造函数进行初始化
- 如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化
对象数组初始化:
- 对象数组的初始化过程,实际上就是调用构造函数对每一个数组元素进行初始化的过程。
- 如果在声明数组时给出每一个数组元素的初始值,在初始化过程中就会调用最匹配的构造函数。
#include <iostream>
using namespace std;
class Point {
public:
Point() {
x = 0;
y = 0;
}
Point(float a) {
x = a;
y = 0;
}
Point(float a, float b) {
x = a;
y = b;
}
float getX() const { return x; }
float getY() const { return y; }
private:
float x;
float y;
};
int main() {
Point array[3] = {
Point(3, 4), // 初始化 array[0] 调用的是 Point(float, float) 构造函数
5, // 初始化 array[1] 调用的是 Point(float) 构造函数
// 初始化 array[2] 调用的是 Point() 构造函数
};
for (int i = 0; i < 3; i++) {
cout << "array[" << i << "] = (" << array[i].getX() << ", "
<< array[i].getY() << ")" << endl;
}
return 0;
}
/* 运行结果:
array[0] = (3, 4)
array[1] = (5, 0)
array[2] = (0, 0) */
对象指针加减操作
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v) { value = v; }
void show() { cout << value << endl; }
};
int main() {
Counter array[3] = {5, 6, 7};
Counter* p = array; // 或 p = &array[0];
p->show(); // => 5
p++;
p->show(); // => 6
p++;
p->show(); // => 7
return 0;
}
静态成员
- 当用关键字 static 说明一个类成员时,该成员称为静态成员
- 静态成员分为:
- 静态数据成员(也称“静态成员属性”)
- 静态成员函数(也称“静态成员方法”)
- 类的所有对象共享静态数据成员,因此无论建立多少个该类的对象,静态数据成员只有一份拷贝
- 静态数据成员属于类,而不属于具体的对象
- 静态数据成员也有 public 和 private 之分
- 在类外只能访问 public 属性的静态数据成员
- 在类内可以访问所有属性的静态数据成员
- 静态成员是属于类的
- 在类外访问 public 属性的静态数据成员的写法
类名::静态数据成员名
- 当类对象不存在时,也可以访问类的静态数据成员
- 在类外访问 public 属性的静态数据成员的写法
- 静态成员函数只属于一个类,它没有 this 指针
- 静态成员函数也可以声明为 public 或 private 属性
class ABCD {
int value;
public:
static int s_value; // 在类内声明静态成员属性
};
int ABCD::s_value = 6; // 初始化静态成员属性
void main() {
ABCD A, B, C, D;
}
#include <iostream>
using namespace std;
class Counter {
static int count; // 声明静态成员
public:
void setCount(int num) { count = num; }
void showCount() { cout << count << " " << endl; }
};
int Counter::count = 0; // 初始化静态成员
int main() {
Counter a, b;
a.showCount(); // => 0
b.showCount(); // => 0
a.setCount(34);
a.showCount(); // => 34
b.showCount(); // => 34
return 0;
}
/* notes:
虽然对象无法直接访问类身上的静态成员,但对象可以通过非静态方法获取到类身上的静态成员。
*/
构造函数声明为非 public
- 构造函数可以声明为非 public,比如可以声明为 private 或 protected,这样就只能在类内部或派生类中使用,而不能在类外部直接调用。
- 这种方式可以用来实现单例模式等设计模式,以及限制用户对类进行实例化等需求。
- 如果构造函数被声明为非 public,那么用户就不能直接实例化对象了,需要提供其他方法来获取对象。
单例模式(Singleton Pattern)
如果有一个类 MyClass,如何设计才能保证 在程序运行中该类只能有一个实例?
#include <iostream>
using namespace std;
class MyClass {
private:
static MyClass* ins;
MyClass() {}
public:
static MyClass* getIns() {
if (ins == NULL)
ins = new MyClass();
return ins;
}
};
MyClass* MyClass::ins = NULL;
int main() {
MyClass *obj1, *obj2;
obj1 = MyClass::getIns();
obj2 = MyClass::getIns();
cout << (obj1 == obj2 ? "obj1 == obj2" : "obj1 != obj2") << endl; // => obj1 == obj2
}
#include <iostream>
using namespace std;
class MyClass {
private:
static MyClass ins;
MyClass() {}
public:
static MyClass& getIns() {
return ins;
}
};
MyClass MyClass::ins;
int main() {
MyClass& obj1 = MyClass::getIns();
MyClass& obj2 = MyClass::getIns();
cout << (&obj1 == &obj2 ? "&obj1 == &obj2" : "&obj1 != &obj2") << endl; // => &obj1 == &obj2
return 0;
}
常对象与常成员(const)
- 如果某个对象不允许被修改,则该对象称为常对象。C++ 用关键字 const 来定义常对象。
- const 也可以用来限定类的数据成员和成员函数,分别称为类的常数据成员和常成员函数。
- 常对象和常成员明确规定了程序中各种对象的变与不变的界线,从而进一步增强了 C++ 程序的安全性和可控性。
- 定义常对象的语法:
类型 const 对象名;
const 类型 对象名;
- 常对象不能变,只能调用常成员函数
- 常对象的应用
- 函数返回值
- 函数形参(常引用)
- 常数据成员只能通过初始化列表来获得初值
- 静态常数据成员在类外说明和初始化
- 常成员函数
- 语法:
返回类型 成员函数名(参数表) const;
- 不能修改对象数据成员的值
- 不能调用该类中没有用 const 修饰的成员函数
- 常对象只能调用它的常成员函数,而不能调用其他成员函数
- const 关键字可以用于参与重载函数的区分
- 语法:
#include <iostream>
using namespace std;
class Clock {
private:
int hour, minute, second;
public:
Clock(int h, int m, int s);
void SetTime(int h, int m, int s);
void ShowTime(void);
};
Clock::Clock(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
void Clock::SetTime(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
void Clock::ShowTime() {
cout << "Time is: " << hour << ":" << minute << ":" << second << endl;
}
int main(void) {
const Clock C1(9, 9, 9);
Clock const C2(10, 10, 10);
Clock C3(11, 11, 11);
// C1 = C3; // => 错误
// C1.ShowTime(); // => 错误
C3.ShowTime();
// C1.SetTime(0, 0, 0); // => 错误
return 0;
}
C1 = C3;
C1 是常对象,不能被赋值C1.ShowTime();
C1 为常对象,不能访问非常成员函数C1.SetTime(0, 0, 0);
C1 为常对象,不能被更新
#include <iostream>
using namespace std;
class A {
private:
const int& r; // 常引用数据成员
const int a; // 常数据成员
static const int b; // 静态常数据成员
public:
A(int i) : a(i), r(a){}; // 常数据成员只能通过初始化列表来获得初值
void display() { cout << "a = " << a << ", r = " << r << endl; }
};
const int A::b = 3; // 静态常数据成员在类外说明和初始化
int main(void) {
A a1(1), a2(2);
a1.display(); // => a = 1, r = 1
a2.display(); // => a = 2, r = 2
return 0;
}
A(int i) : a(i), r(a){};
常数据成员只能通过初始化列表来获得初值
- 常数据成员在构造函数中是不能被修改的,而初始化列表是在构造函数之前被执行的
- 只有通过初始化列表来为常数据成员赋初值才能保证其在构造函数中的值不会被修改
- 如果试图在构造函数中为常数据成员赋值,则会导致编译错误
#include <iostream>
using namespace std;
class Date {
private:
int Y, M, D;
public:
Date() : Y(0), M(0), D(0) {} // 默认构造函数
int year() const;
int month() const;
int day() const { return D; };
int day() { return D++; }
int AddYear(int i) { return Y + i; };
};
// int Date::month() { return M; } // 错误:常成员函数实现不能缺少 const
// int Date::year() const { return Y++; } // 错误:常成员函数不能更新类的数据成员
int Date::year() const { return Y; }
int main(void) {
Date const d1;
int j = d1.year(); // 正确
// int j = d1.AddYear(10); // 错误:常对象不能调用非常成员函数
Date d2;
int i = d2.year(); // 正确,非常对象可以调用常成员函数
int k = d2.day(); // 正确,非常对象可以调用非常成员函数
cout << "j = " << j << ", i = " << i << ", k = " << k << endl; // j = 0, i = 0, k = 0
return 0;
}
友元
- 封装的目的就是为了实现信息隐蔽
- 一个对象的私有成员只能被自己的成员访问到。当类外的对象或函数要访问这个类的私有成员时,只能通过该类提供的公有成员间接地进行。
- C++ 提供了友元机制来打破私有化的界限,即一个类的友元可以访问到该类的私有成员。
计算平面上两点之间的距离
#include <iostream>
#include <math.h>
using namespace std;
class Point {
float x, y;
public:
Point(float xx = 0, float yy = 0) {
x = xx;
y = yy;
}
float GetX() { return x; }
float GetY() { return y; }
};
float Distance(Point a, Point b) {
float x1, x2, y1, y2, dx, dy;
x1 = a.GetX();
y1 = a.GetY();
x2 = b.GetX();
y2 = b.GetY();
dx = x1 - x2;
dy = y1 - y2;
return sqrt(dx * dx + dy * dy);
}
int main() {
Point p1(3.0, 5.0), p2(4.0, 6.0);
float d = Distance(p1, p2);
cout << "The distance is " << d << endl; // => The distance is 1.41421
return 0;
}
#include <iostream>
#include <math.h>
using namespace std;
class Point {
float x, y;
public:
Point(float xx = 0, float yy = 0) {
x = xx;
y = yy;
}
float GetX() { return x; }
float GetY() { return y; }
float Distance(Point a) {
float x1, y1, dx, dy;
x1 = a.GetX();
y1 = a.GetY();
dx = x1 - x;
dy = y1 - y;
return sqrt(dx * dx + dy * dy);
}
};
int main() {
Point p1(3.0, 5.0), p2(4.0, 6.0);
float d = p1.Distance(p2);
cout << "The distance is " << d << endl; // => The distance is 1.41421
return 0;
}
虽然从语法的角度来看这不难实现,但是理解起来却有问题:距离反映的是两点之间的关系,它既不属于每一个单独的点,也不属于整个 Point 类。
#include <iostream>
#include <math.h>
using namespace std;
class Point {
float x, y;
public:
Point(float xx = 0, float yy = 0) {
x = xx;
y = yy;
}
float GetX() { return x; }
float GetY() { return y; }
friend float Distance(Point a, Point b); // 友元函数
};
float Distance(Point a, Point b) {
float dx, dy;
dx = a.x - b.x; // 友元函数可以直接访问私有成员
dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int main() {
Point p1(3.0, 5.0), p2(4.0, 6.0);
float d = Distance(p1, p2); // 和普通函数一样,可以直接调用友元函数
cout << "The distance is " << d << endl; // => The distance is 1.41421
return 0;
}
friend float Distance(Point a, Point b);
友元函数
- 友元函数不属于任何类,因此友元函数没有 this 指针
- 友元函数的声明可以放在类内的任何位置
友元类
class Y {
// ...
};
class X {
// ...
friend class Y;
};
- 除了将一个普通函数声明为一个类的友元函数外,也可以将一个类 Y 声明为另一个类 X 的友元类。
- 友元类的特点:类 Y 中的所有成员函数都成为类 X 的友元函数,都能直接访问类 X 中所有的成员。
- 友元的声明必须放在类的内部,但放在哪个段没有区别。
#include <iostream>
#include <math.h>
using namespace std;
class Y; // 向前说明
class X {
int x;
friend class Y;
public:
void show() { cout << "x = " << x << endl; }
};
class Y {
public:
void SetX(X& obj, int v) { obj.x = v; }
};
int main() {
X xobj;
Y yobj;
yobj.SetX(xobj, 5);
xobj.show(); // => x = 5
return 0;
}
class Y; // 向前说明
- 在 C++ 中,如果要使用某个类的成员,需要先声明这个类,否则编译器会报错
- 因为编译器需要知道这个类的成员的类型和作用,才能正确编译代码
- 向前声明就是为了在使用某个类的成员时,能够告诉编译器这个类的存在,从而避免编译错误
#include <iostream>
#include <math.h>
using namespace std;
class X; // 向前说明
class Y {
public:
void SetX(X& obj, int v);
};
class X {
int x;
friend class Y;
friend void Y::SetX(X& obj, int v);
public:
void show() { cout << "x = " << x << endl; }
};
void Y::SetX(X &obj, int v) { obj.x = v; }
int main() {
X xobj;
Y yobj;
yobj.SetX(xobj, 5);
xobj.show(); // => x = 5
return 0;
}
友元关系不具备对称性、传递性
- 友元关系不具备对称性,即 X 是 Y 的友元,但 Y 不一定是 X 的友元。
- 友元关系不具备传递性,即 X 是 Y 的友元,Y 是 Z 的友元,但 X 不一定是 Z 的友元。
组合类
一个类的对象作为另一个类的成员,这体现的是整体和部分的关系,即对象的包含关系,这个作为成员的对象被称为子对象。
#include <iostream>
using namespace std;
class Point {
float x, y;
public:
Point(float xx, float yy) {
x = xx;
y = yy;
}
float GetX() { return x; }
float GetY() { return y; }
void moveto(float xx, float yy) {
x = xx;
y = yy;
}
};
class Circle {
Point center;
float radius;
public:
Circle(float x, float y, float r) : center(x, y) { radius = r; }
void moveto(float xx, float yy) { center.moveto(xx, yy); }
float GetCenterX() { return center.GetX(); }
float GetCenterY() { return center.GetY(); }
float GetRadius() { return radius; }
};
int main() {
Circle acircle(0, 0, 5);
acircle.moveto(5, 8);
cout << "圆心坐标为 (" << acircle.GetCenterX() << ", "
<< acircle.GetCenterY() << ")" << endl; // => 圆心坐标为 (5, 8)
cout << "半径为 " << acircle.GetCenterY() << endl; // => 半径为 8
return 0;
}
Circle(float x, float y, float r) : center(x, y) { radius = r; }
center(x, y)
若子对象对应的类的构造函数有参数,那么包含该子对象的类必须使用表达式的方式先初始化子对象。
8.3.6 练习题 | 汽车类
- 假设我们有发动机类 motor,车门类 doors,车轮类 wheels,每个类都有构造函数打印输出类名,每个类有析构函数打印输出析构类名。现在请用这些类组合成汽车类 car,汽车类也有自己的构造与析构函数,打印输出构造与析构汽车类。
- 请在主函数中生成汽车类对象,观察程序的输出,体会构造与析构函数的调用顺序。
#include <iostream>
using namespace std;
class Motor {
public:
Motor() { cout << "Motor constructed." << endl; }
~Motor() { cout << "Motor destructed." << endl; }
};
class Doors {
public:
Doors() { cout << "Doors constructed." << endl; }
~Doors() { cout << "Doors destructed." << endl; }
};
class Wheels {
public:
Wheels() { cout << "Wheels constructed." << endl; }
~Wheels() { cout << "Wheels destructed." << endl; }
};
class Car {
private:
Motor m;
Doors d;
Wheels w1, w2;
public:
Car() { cout << "Car constructed." << endl; }
~Car() { cout << "Car destructed." << endl; }
};
int main() {
Car c;
return 0;
}
/*
Motor constructed.
Doors constructed.
Wheels constructed.
Wheels constructed.
Car constructed.
Car destructed.
Wheels destructed.
Wheels destructed.
Doors destructed.
Motor destructed.
*/