Cpp复习 Chapter 3 对象和类


Cpp系列笔记目录

【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板
【Cpp筑基】二、声明 vs 定义、头文件、存储持续性作用域和链接性、名称空间
【Cpp筑基】三、对象和类
【Cpp筑基】四、重载运算符、友元、类的转换函数
【Cpp筑基】五、类的继承、虚函数、抽象基类


1. 类

Cpp相比于C语言新增了类的概念,因此Cpp是一种面向对象的编程语言而C语言只是一种面向过程的编程语言。类是一种将抽象转换为用于自定义的Cpp工具,它将数据表示和操纵数据的方法组合成一个整洁的包。

一般来说,类规范由两部分组成:

  • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
  • 类的方法定义:描述如何实现类成员函数。

1.1 类的一个简单示例

简单来说,类声明提供了类的蓝图,而方法定义则提供了类实现的细节。举一个例子:

my_class.h文件中存放了类的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// my_class.h
#ifndef MY_CLASS_H
#define MY_CLASS_H

#include <iostream>
#include <string>

// 类声明
class Person {
public:
// 构造函数
Person(const std::string &name, int age);
// 公有接口:成员函数
void display() const;
void setAge(int age);
int getAge() const;
private:
// 数据成员
std::string name;
int age;
};

#endif

my_class.cpp中存放了类的方法定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// my_class.cpp
#include "my_class.h"
// 构造函数实现
Person::Person(const std::string &name, int age) : name(name), age(age) {}

// display 方法实现
void Person::display() const {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}

// setAge 方法实现
void Person::setAge(int age) {
if (age > 0) {
this->age = age;
} else {
std::cout << "Invalid age!" << std::endl;
}
}

// getAge 方法实现
int Person::getAge() const {
return age;
}

1.2 类的访问权限

Cpp中类的访问权限有三种:

  • public,公有,类的成员可以从类的外部直接访问。任何对象或函数都可以访问public成员
  • protected,保护,类的成员不能从类的外部直接访问,但可以被派生类(继承该类的类)访问。
  • private,私有,访问权限表示成员不能从类的外部直接访问,也不能被派生类访问。只有类的成员函数和友元函数可以访问private成员。

需要注意的是,==在Cpp中访问权限是相对类的外部而言的,在类的内部来说,所有的变量和成员函数都是可以互相访问的==。公有public方法也可以访问私有private成员变量和方法,Cpp类的成员函数,无论其访问级别如何,都可以访问该类中的所有成员,包括私有成员,这是因为成员函数被认为是类的一部分,拥有该类的所有细节的权限。

在Cpp中可以使用关键字struct或者class来声明一个类,它们的使用方式基本相同,唯一不同之处在于struct声明的成员默认访问权限为public,而class声明的成员默认访问权限是private

数据隐藏作为面向对象编程(Object Orient Program,OOP)的主要目的,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分;否则就无法从程序中调用这些函数。通常,我们使用私有成员函数来处理不属于公有接口的实现细节。

1.3 内联方法

==如果在一个类中,其成员函数的定义放在类的声明中,那么该成员函数将被自动转化为内联函数,以减小开销==。内联函数是一种建议编译器在每次调用该函数时,将其代码插入到调用点处,而不是进行常规的函数调用。这可以减少函数调用的开销,提高程序执行效率,尤其是对于短小的函数。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

class MyClass {
public:
// 在类声明中定义的函数,自动转化成内联函数
void printMessage() const {
std::cout << "Hello, Inline Function!" << std::endl;
}
};

int main() {
MyClass obj;
obj.printMessage(); // 调用内联函数
return 0;
}

当然我们也可以手动在类声明之外定义内联成员函数。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

class MyClass {
public:
void printMessage() const; // 声明时不需要 inline
};

// 定义时使用 inline
inline void MyClass::printMessage() const {
std::cout << "Hello, Inline Function!" << std::endl;
}

int main() {
MyClass obj;
obj.printMessage();
return 0;
}

使用new初始化类,使用newheap区上分配内存来初始化一个类的指针会调用相应的构造函数,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

class Complex {
private:
double real, imag;

public:
// 默认构造函数
Complex() : real(0), imag(0) {
std::cout << "Default constructor called" << std::endl;
}

// 带参数的构造函数
Complex(double r, double i) : real(r), imag(i) {
std::cout << "Parameterized constructor called" << std::endl;
}

// 析构函数
~Complex() {
std::cout << "Destructor called" << std::endl;
}

// 显示函数
void display() const {
std::cout << "(" << real << ", " << imag << ")" << std::endl;
}
};

int main() {
// 使用 new 动态分配对象
Complex* c1 = new Complex(); // 调用默认构造函数
Complex* c2 = new Complex(3.0, 4.0); // 调用带参数的构造函数

// 使用对象
c1->display();
c2->display();

// 释放动态分配的内存
delete c1; // 调用析构函数并释放内存
delete c2; // 调用析构函数并释放内存

return 0;
}

1.4 构造函数和析构函数

在C++中,构造函数和析构函数是特殊的成员函数,用于在对象的生命周期内执行初始化和清理操作。构造函数在对象创建时自动调用,而析构函数在对象销毁时自动调用。

==构造函数是用于初始化对象的特殊成员函数。构造函数的名称与类名相同,并且没有返回类型。构造函数可以被重载,即一个类可以有多个构造函数,只要它们的参数(特征标)不同==。

构造函数的特点是:

  • 名称与类名相同
  • 没有返回类型
  • 在对象创建时自动调用
  • 可以被重载
  • 可以使用初始化列表
  • 不能成为虚函数

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

class MyClass {
public:
int x;
private:
// 默认构造函数
MyClass() {
x = 0;
std::cout << "Default Constructor called" << std::endl;
}
// 参数化构造函数
MyClass(int value) {
x = value;
std::cout << "Parameterized Constructor called" << std::endl;
}
// 拷贝构造函数
MyClass(const MyClass& obj) {
x = obj.x;
std::cout << "Copy Constructor called" << std::endl;
}
};

int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用参数化构造函数
MyClass obj3 = obj2; // 调用拷贝构造函数
return 0;
}

这里重载了构造函数,提供了三种构造函数,当类被示例化的时候会从构造函数中选择符合特征标的构造函数进行调用。

成员名称与参数名,不熟悉构造函数的程序员可能会将类成员名称用作构造函数的参数名,例如:

1
2
3
4
5
6
7
8
class MyClass {
public:
int x; // 成员变量

MyClass(int x) { // 构造函数参数名与成员变量名相同
x = x; // 这是错误的
}
};

为了解决这种问题,我们可以在数据成员名前面增加_或者一些其他的标识,例如:

1
2
3
4
5
6
7
8
class MyClass {
public:
int _x; // 成员变量

MyClass(int x) { // 这是可行的,没有名称冲突
_x = x;
}
};

或者使用this指针来显示地指明

1
2
3
4
5
6
7
8
class MyClass {
public:
int x; // 成员变量

MyClass(int x) { // 这是可行的,使用this指针
this->x = x;
}
};

或者使用==构造函数初始化列表==,例如:

1
2
3
4
5
6
7
8
class MyClass {
public:
int x; // 成员变量

MyClass(int x) : x(x) { // 使用初始化列表
// 这样更直观,并且避免了赋值操作
}
};

如果Classy是一个类,且mem1mem2是其成员变量,那么成员初始化列表的语法是:

1
Classy::Classy(int n, int m): mem1(n), mem2(m){ }

从概念上来说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码,请注意以下几点:

  • 这种初始化格式只能适用于构造函数;
  • 必须使用这种格式来初始化非静态const数据成员;
  • 必须使用这种格式来初始化引用数据成员。

原因在于const变量和&引用都只能在被创建的时候进行初始化,举个例子:

  1. 初始化非静态const数据成员
1
2
3
4
5
6
class MyClass{
private:
const int qsize;
public:
MyClass(int qs): qsize(qs) {}
}
  1. 初始化引用数据成员
1
2
3
4
5
6
7
class Agency {...};
class Agent{
private:
Agency & belong; // must be initializer list to initialize
public:
Agent(Agency& a): belong(a) {...}
}

==如果没有提供构造函数,那么编译器会为我们添加一个默认构造函数,只不过这个构造函数什么也不执行;一个默认拷贝构造函数,并且重载赋值运算符=,允许我们直接拷贝已有的对象实例==。

默认的构造函数,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

class MyClass {
public:
int x;
double y;
};

int main() {
MyClass obj; // 使用编译器生成的默认构造函数
std::cout << "x: " << obj.x << ", y: " << obj.y << std::endl; // 未定义行为
return 0;
}

在这个类MyClass中,我们并没有定义构造函数,编译器自动帮我们生成了一个构造函数,这个构造函数什么都不做,所以xy是没有赋值的,在最后一行的打印语句中将会报错。

默认拷贝构造函数,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

class MyClass {
public:
int x;
double y;
};

int main() {
MyClass obj1;
obj1.x = 10;
obj1.y = 20.5;

MyClass obj2 = obj1; // 使用编译器生成的默认拷贝构造函数
std::cout << "obj2.x: " << obj2.x << ", obj2.y: " << obj2.y << std::endl;
return 0;
}

在这个类MyClass中,我们并没有定义构造函数,编译器自动帮我们生成了一个拷贝构造函数,编译器生成的默认拷贝构造函数会逐个成员地进行浅拷贝。

重载运算符operator=,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

class MyClass {
public:
int x;
double y;
};

int main() {
MyClass obj1;
obj1.x = 10;
obj1.y = 20.5;

MyClass obj2;
obj2 = obj1; // 使用编译器生成的默认赋值运算符
std::cout << "obj2.x: " << obj2.x << ", obj2.y: " << obj2.y << std::endl;
return 0;
}

编译器生成的默认赋值运算符(operator=)也会逐个成员地进行浅拷贝。

==在Cpp中,析构函数是一个特殊的成员函数,用于在对象生命周期结束时执行清理工作,析构函数通常用于释放资源(如动态内存、文件句柄、网络连接等),确保对象在销毁时不会造成资源泄露==。

析构函数的特点是:

  • 名称与类名相同,但前面加上波浪号~
  • 没有返回类型
  • 没有参数
  • 在对象生命周期结束时自动调用
  • 不能被重载,一个类只能有一个析构函数
  • 不能成为虚函数

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class MyClass {
public:
int* ptr;

MyClass(int size) {
ptr = new int[size];
std::cout << "Constructor called" << std::endl;
}

~MyClass() {
delete[] ptr;
std::cout << "Destructor called" << std::endl;
}
};

int main() {
MyClass obj(10); // 创建对象并分配内存
// 离开作用域时自动调用析构函数,释放内存
return 0;
}

如果你没有定义析构函数,那么编译器将会自动生成一个析构函数,该析构函数不执行任何操作,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

class Simple {
public:
int x;

Simple() : x(0) {
std::cout << "Simple Constructor" << std::endl;
}

// 编译器会生成一个默认析构函数
// ~Simple() { }
};

int main() {
Simple obj; // 创建对象时调用构造函数
return 0; // 离开作用域时调用编译器生成的默认析构函数
}

1.5 C++11列表初始化

在C++11中,列表初始化语法也可以用于类中,只要提供与某个构造函数的参数列表匹配的内容,就可使用大括号{}进行列表初始化。

列表初始化类的对象,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class MyClass {
public:
int x;
double y;
// 默认构造函数
MyClass(){}
// 构造函数
MyClass(int a, double b) : x(a), y(b) {
std::cout << "MyClass constructed with x: " << x << ", y: " << y << std::endl;
}
};

int main() {
MyClass jock {}; //使用默认构造函数进行初始化
MyClass alex {10, 2.5}; //使用构造函数进行初始化
MyClass john {2, 5.1}; //使用构造函数进行初始化
return 0;
}

列表初始化类的对象的数组,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

class MyClass {
public:
int x;
double y;
// 默认构造函数
MyClass(){}
// 构造函数
MyClass(int a, double b) : x(a), y(b) {
std::cout << "MyClass constructed with x: " << x << ", y: " << y << std::endl;
}
};

int main() {
// 使用列表初始化来初始化类对象数组
MyClass arr[3] = {
{1, 2.5},
{3, 4.5},
{5, 6.5},
};
// 也可这样来初始化数组
MyClass arry[3] = {
MyClass(2, 2.5),
MyClass(3, 1.5),
MyClass(4, 6.5),
};
return 0;
}

1.6 const成员函数

在C++中,const成员函数是指那些不会修改类的成员变量的成员函数。通过在函数声明的末尾加上 const关键字,可以保证该函数不会修改对象的状态。这对于确保某些函数的调用不会意外地改变对象的状态非常有用,并且在设计接口和维护代码的稳定性方面起着重要作用。

const成员函数的基本使用,举个例子:

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
int getValue() const; // 声明为const成员函数
private:
int value;
};

int MyClass::getValue() const {
return value; // 可以读取成员变量
// value = 10; // 错误:不能修改成员变量
}

const对象和成员函数const对象不能调用非const成员函数,而非const对象可以调用所有的成员函数,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyClass {
public:
int getValue() const;
void setValue(int v);
private:
int value;
};

int MyClass::getValue() const {
return value;
}

void MyClass::setValue(int v) {
value = v;
}

int main() {
MyClass obj;
obj.setValue(10);
std::cout << obj.getValue() << std::endl; // 可以调用非const对象的所有成员函数

const MyClass constObj;
// constObj.setValue(10); // 错误:不能调用非const成员函数
std::cout << constObj.getValue() << std::endl; // 只能调用const成员函数

return 0;
}

1.7 this指针

在C++中,this指针是一个隐含在每个非静态成员函数中的特殊指针。它指向调用成员函数的对象,并且允许成员函数访问调用对象的成员变量和其他成员函数。this指针的主要作用是区分成员变量和局部变量,并在成员函数内部引用对象本身。

this指针的特点

  • 指向调用对象:this指针指向调用成员函数的对象本身。
  • 只在非静态成员函数中可用:静态成员函数没有this指针,因为静态成员函数是类级别的,而不是对象级别的。
  • 类型:this指针的类型是指向类类型的常量指针,例如对于类MyClassthis指针的类型是MyClass* const
  • 常量性:this指针是一个常量指针,不能更改它指向的对象,但可以用来修改对象的成员。

使用this指针区分成员变量和局部变量,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

class MyClass {
private:
int value;
public:
MyClass(int value) {
// 使用this指针区分成员变量和构造函数参数
this->value = value;
}
void setValue(int value) {
// 使用this指针区分成员变量和函数参数
this->value = value;
}
void printValue() const {
std::cout << "Value: " << this->value << std::endl;
}
};

int main() {
MyClass obj(10);
obj.printValue();
obj.setValue(20);
obj.printValue();
return 0;
}

使用this指针实现链式调用(返回*this,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class MyClass {
private:
int value;
public:
MyClass(int value) : value(value) {}
MyClass& setValue(int value) {
this->value = value;
return *this; // 返回对象本身的引用
}
MyClass& incrementValue() {
++this->value;
return *this; // 返回对象本身的引用
}
void printValue() const {
std::cout << "Value: " << this->value << std::endl;
}
};
int main() {
MyClass obj(10);
obj.setValue(20).incrementValue().printValue(); // 链式调用
return 0;
}

在这个例子中,我们实现了对象方法的链式调用,但链式调用更多地用在运算符重载当中,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class MyClass {
private:
int value;

public:
MyClass(int value) : value(value) {}
MyClass& operator+=(int other) {
this->value += other;
return *this; // 返回*this
}
void printValue() const {
std::cout << "Value: " << this->value << std::endl;
}
};
int main() {
MyClass obj(10);
obj += 5;
obj.printValue(); // 输出:Value: 15
return 0;
}

Reference

《C++ Primer Plus》