6.Introduction to Class

一、Classes简介

  • 头文件(.h)VS 源码(.cpp)
  • 构造函数
  • 析构函数
  • 操作符重载
  • const

1.1 头文件(.h)

1.2 源码(.cpp, .cc, etc.)

1.3 构造函数

构造函数可分为带参数和不带参数两种类型,其调用方式示例如下:

1
2
FMRadio myRadio(88.5, 5); // 使用带参数的构造函数创建对象
FMRadio myRadio; // 使用不带参数的构造函数创建对象

但是,使用下列代码并不能正确调用不带参数的构造函数:

1
FMRadio myRadio(); // 问题:这会被误解为一个函数声明

该语句被编译器解释为一个函数声明,而非使用默认构造函数实例化对象。为了调用默认构造函数,应直接使用对象名而不加任何括号;若需调用带参数的构造函数,则需在括号内指定参数。

关于默认构造函数

当一个类被定义但未显式声明任何构造函数时,C++会自动生成一个默认无参构造函数,这个构造函数基本上不执行任何操作。

但是,一旦在类中定义了至少一个构造函数(无论是带参数还是不带参数的),C++将不再自动生成默认的无参构造函数。这可能导致在未明确定义无参构造函数的情况下尝试实例化类时遇到问题,如下例所示:

1
2
3
4
5
6
7
class FMRadio { 
public:
FMRadio(double freq, int vol); // 自定义带参数构造函数
// ...
};

FMRadio myRadio; // 问题:类未定义默认无参构造函数,此行代码会引发错误

在这种情况下,若需要使用无参构造函数,必须自行定义之,或确保使用已定义的带参数构造函数进行对象的初始化。

1.4 析构函数

1.5 操作符重载

二、Const Correctness

2.1 Const Member Functions

  • 类通常包含const和非const成员函数。const对象仅可调用const成员函数。
  • 在const成员函数内,类的所有成员变量都被视作const,允许读取但禁止修改。
  • const成员函数不能调用非const成员函数,以防止间接修改对象状态。

示例:定义Point类的const成员函数

1
2
3
4
5
6
7
8
9
10
11
class Point {
public:
Point(double x, double y); // 构造函数
double getX() const; // const成员函数,不修改任何成员变量
private:
double x, y;
};

double Point::getX() const {
return x; // 仅返回成员变量x的值
}

2.2 Const References

  • 使用const引用作为函数参数可以防止该函数修改传入的对象。
  • 即使传递给函数的原始对象不是const,函数内部也会将其视为const,确保对象内容的不可变性。
  • 可以将非const对象传递给接受const引用的函数,但不能将const对象传递给仅接受非const引用的函数,因为这违反了const的约束。

示例:使用const引用作参数的函数

1
2
3
void PrintVector(const vector<int>& vec) {
// 在这里,vec被视为const,因此不能对其进行修改
}
  • 这种设计允许既可以传递const对象也可以传递非const对象到函数PrintVector,但在函数内部,vec始终被视作const,保障了数据的安全性和不变性。
  • 采用const引用作为参数的做法不仅可以提高程序的安全性,还可以避免不必要的对象复制,提高效率。

2.3 Const and Pointers

在C++中,const和指针结合使用时,主要涉及两种情形:指向常量的指针(pointer-to-const)和常量指针(const pointer)。

  • 指向常量的指针(pointer-to-const):这种指针可以指向一个常量数据,不允许通过指针修改其指向的数据,但允许指针本身改变,即可以指向另一块数据。
    • 声明方式:const Type* myPointerType const* myPointer
    • 关键点:指针可以重新指向不同的地址,但不能通过指针修改所指向的值。
  • 常量指针(const pointer):这种指针的指向一旦设定就不可改变,但它指向的数据值是可以修改的(除非指向的数据本身被声明为const)。
    • 声明方式:Type* const myConstPointer
    • 关键点:指针的指向固定不变,但所指向的数据可以修改。

区分这两者的简单方法是观察const关键字与*符号的位置关系:

  • 指向常量的指针const位于*的左侧。
  • 常量指针const位于*的右侧。

技巧:从右向左读变量声明可以帮助记忆:

  • const int* ptr:从右向左读为 "ptr is a pointer to a int that is const"
  • int * const ptr:从右向左读为 "ptr is a const pointer to a int"

指向常量的指针既可以指向const变量,也可以指向非const变量;而常量指针则主要关注指针本身的不变性,而非所指数据的不变性。

总结:

2.4 Const Iterators

迭代器在C++中被广泛用于遍历容器元素,其用法和行为与指针非常相似。然而,迭代器与指针在const性质上的表现有所不同。

  • 迭代器的const性质:
    • 声明为const vector<int>::iterator itr的迭代器,其行为类似于常量指针int* const itr,而不是指向常量的指针const int* itr。这意味着你可以修改迭代器指向的值,但不能改变迭代器本身的指向。
    • 要创建一个只能读取元素但不能修改它们的迭代器(类似于const int* itr),应使用const_iterator

示例说明

1
2
3
4
5
6
7
8
9
vector<int> v{1, 2312};
const vector<int>::iterator itr = v.begin(); // itr的指向不可变,但可以修改其指向的元素
*itr = 5; // 正确:可以修改itr指向的元素
++itr; // 错误:itr是const,不能改变指向

vector<int>::const_iterator citr = v.begin(); // citr可以改变指向,但不可以修改其指向的元素
*citr = 5; // 错误:citr指向的元素不可修改
++citr; // 正确:可以改变citr的指向
int value = *citr; // 正确:可以读取citr指向的元素
  • 对于const对象或const引用的容器,应使用const_iterator来遍历其元素,以确保遍历过程中不会意外修改容器内容。
  • const_iterator的支持是通过容器类中const重载的begin()end()方法实现的,这些方法在容器对象为const时返回const_iterator,确保了只读访问。

容器类的const_iterator实现

1
2
3
4
5
6
7
8
9
10
11
template <typename T> class vector {
public:
// 返回可修改元素的迭代器
iterator begin();
iterator end();

// 返回只读迭代器,用于const对象
const_iterator begin() const;
const_iterator end() const;
// ...其他成员函数...
};

通过这种方式,C++为容器元素的遍历提供了灵活的读写控制,同时确保了代码的安全性和可维护性。

2.5 Mutable关键字

在C++中,const成员函数表明该函数不会修改对象的任何成员变量。然而,有些情况下,我们可能需要在const成员函数内部修改某些成员变量的状态。这时,mutable关键字就派上用场了。

使用mutable声明的成员变量可以在对象的const成员函数中被修改。这允许开发者在不破坏对象外部不可变承诺的前提下,灵活处理内部状态。

考虑一个需要缓存昂贵计算结果的类CachedData。为了在对象被声明为const时仍然能更新缓存,我们可以将缓存相关的成员变量声明为mutable

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CachedData {
public:
CachedData() : value(0), cacheValue(0), cacheValid(false) {}

void compute() const { // 假设为const成员函数
if (!cacheValid) {
cacheValue = /* expensive computation */ value; // 计算并缓存结果
cacheValid = true; // 标记缓存为有效
}
}

int getValue() const { return cacheValue; } // 返回缓存值

private:
int value; // 原始数据
mutable int cacheValue; // 可在const成员函数中修改的缓存值
mutable bool cacheValid; // 同上,标记缓存是否有效
};

在这个例子中,尽管CachedData的实例可能被声明为constcompute方法仍可更新cacheValuecacheValid。这是因为这两个成员变量被声明为mutable,从而使得我们能够在保持对象外观不变性的同时,提高内部处理效率和灵活性。

三、构造函数:成员初始化列表

3.1 C++对象构造过程

在C++中,对象的构造过程,如果不借助成员初始化列表,一般遵循以下步骤:

  1. 内存分配:对象所需的内存空间被分配,此时成员变量尚未初始化,可能含有未定义的值。
  2. 默认构造:为成员变量调用默认构造函数。对于基本类型,这一步不改变其未定义的状态;对于类类型,调用其默认构造函数。
  3. 赋值操作:执行构造函数体中的赋值操作,对成员变量进行初始化。

这个过程中,类类型的成员变量可能会经历两次初始化:首先是默认构造,随后是在构造函数体内的显式赋值。

3.2 不使用初始化列表的构造示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SimpleClass {
public:
SimpleClass();

private:
int myInt;
string myString;
vector<int> myVector;
};

SimpleClass::SimpleClass() {
myInt = 5;
myString = "C++!";
myVector.resize(10);
}
  1. 获得足够的空间来容纳对象的所有数据成员。此时数据成员都为垃圾值:

  1. 调用每个实例变量的默认构造函数。对于基本类型而言,依然保持垃圾值:

  1. 调用构造函数来进行初始化:

在这个例子中,myStringmyVector在被赋予新值前,已通过默认构造函数被初始化一次,导致效率降低。

3.3 使用成员初始化列表优化

使用成员初始化列表可以在构造对象时更加高效和直接地初始化成员变量,避免不必要的二次初始化。

改进后的构造函数

1
2
3
SimpleClass::SimpleClass() : myInt(5), myString("C++!"), myVector(10) {
// 构造函数体现在可以为空
}

通过使用成员初始化列表:

  • 成员变量myIntmyStringmyVector在构造函数体执行之前直接初始化为指定的值。
  • 提高了构造过程的效率,尤其是对于类类型成员变量而言,避免了不必要的默认构造和后续赋值步骤。

3.4 使用初始化列表的必要场景

在C++中,某些情况下使用成员初始化列表不仅是提升效率的做法,而且是必须的。这主要包括以下两种情况:

  1. 初始化const成员变量

const成员变量一旦被定义,就必须立即初始化,而且之后不能再被修改。因此,必须在构造函数的初始化列表中对它们进行初始化。如果试图在构造函数体内赋值给const成员变量,编译器会报错。

示例

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
public:
Counter(int maxValue);

private:
int value;
const int maximum; // const成员变量
};

Counter::Counter(int maxValue) : value(0), maximum(maxValue) {
// 构造函数体可以为空,因为所有必要的初始化已在列表中完成
}
  1. 初始化没有默认构造函数的类类型成员

如果类的成员是另一个类的对象,且该类没有提供无参的默认构造函数(或者你需要调用一个特定的构造函数来初始化该成员),那么你必须通过初始化列表来初始化这些成员变量。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ComplexObject {
public:
ComplexObject(int param1, double param2);
// ...
};

class MyClass {
public:
MyClass(int x) : complexObj(x, 3.14) {
// 构造函数体内可进行其他初始化操作
}

private:
ComplexObject complexObj; // 通过初始化列表调用特定构造函数
};

在这些场景中,成员初始化列表是初始化成员变量的唯一方法。它确保了即使在复杂场景下,对象的构造也能正确、高效地完成。

四、使用Static共享类信息

4.1 静态数据成员 (Static Data Members)

静态数据成员允许类的所有实例共享同一份数据。这意味着无论创建了多少个实例,静态成员只有一个副本。修改静态成员的状态将影响到所有实例。

声明和定义

  1. 声明:在类定义中声明静态成员,但不分配存储空间。
1
2
3
4
5
6
class MyClass {
public:
void doSomething();
private:
static int myStaticData; // 声明静态数据成员
};
  1. 定义:在类外部定义静态成员,并初始化其值。此步骤为静态成员分配存储空间。
1
int MyClass::myStaticData = 137; // 定义并初始化静态数据成员

注意:定义时使用类的全限定名(MyClass::myStaticData),并省略static关键字。

4.2 静态成员函数

静态成员函数不依赖于类的实例,因此没有this指针。它们只能访问静态数据成员和其他静态成员函数。

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
public:
Point(int xLoc, int yLoc); // 构造函数
static bool compareTwoPoints(const Point &one, const Point &two);
private:
int x, y; // 实例变量
};

bool Point::compareTwoPoints(const Point &one, const Point &two) {
// 静态成员函数实现
return one.x < two.x || (one.x == two.x && one.y < two.y);
}

静态成员函数可以通过类名直接调用:Point::compareTwoPoints(a, b);

4.3 静态常量 (static const)

静态常量成员在所有实例间共享同一常量值,通常用于定义类级别的常量。

声明和初始化

1
2
3
4
5
6
class ClassConstantExample {
public:
// 公开接口省略
private:
static const int MyConstant = 137; // 声明并初始化静态常量
};

这种初始化方式主要适用于整型常量。对于非整型的静态常量(如doublefloat),你可能需要在类外部进行定义。

五、 static_castconst_cast

在C++中,经常需要为类成员函数提供const和非const版本,以支持不同的使用场景。

std::vectorat()函数就是一个典型例子,其中const版本返回元素的const引用,而非const版本则返回一个可修改的引用。为了减少代码冗余并保持逻辑一致性,可以利用static_castconst_cast进行优雅的实现。

5.1 static_cast

  • 用途static_cast主要用于基本数据类型之间的转换,以及相关类类型之间的向上和向下转换,但不涉及运行时类型检查。
  • 特点:提供编译时类型安全检查,但不如dynamic_cast严格。

5.2 const_cast

  • 用途const_cast专门用于修改类型的constvolatile属性,是实现const和非const成员函数共享实现的关键。
  • 特点:能够添加或去除对象的const性质,但使用时需要谨慎以避免未定义行为。

5.3 示例

假设有一个Vector类,我们希望共享const和非const版本的at()函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vector {
public:
int& at(size_t index); // 非const版本声明
const int& at(size_t index) const; // const版本声明
};

// 非const版本的实现
int& Vector::at(size_t index) {
// 首先将*this静态转换为const版本,然后调用const版本的at()
// 最后,使用const_cast去除返回值的const限定符
return const_cast<int&>(static_cast<const Vector*>(this)->at(index));
}

// const版本的实现
const int& Vector::at(size_t index) const {
// 进行边界检查等逻辑
if (index >= size) {
throw std::out_of_range("Index out of range");
}
return data[index];
}

六、示例代码

点击下载示例代码


6.Introduction to Class
https://ci-tz.github.io/2024/02/08/6-Introduction-to-Class/
作者
次天钊
发布于
2024年2月8日
许可协议