类基础
类的简要概括:
- 一个类就是一个用户自定义类型。
- 一个类由一组成员构成,最常见的成员类别是数据成员和成员函数。
- 成员函数可以定义初始化(创建)、拷贝、移动和清理(析构)等语义。
- 对对象使用
.
(点)访问成员,对指针使用->
(箭头)访问成员。 - 可以为类定义运算符,如
+
、!
和&
。 - 一个类就是一个包含其成员的名字空间。
-
public
成员提供类的接口,private
成员提供实现细节。 -
struct
是成员默认为public
的class
。
默认拷贝
默认情况下,类的对象是可以被拷贝的。这意味着,一个类对象可以通过另一个同类对象的副本来进行初始化。这种拷贝是通过逐个成员进行的,即对象的每个成员都会被单独拷贝。
class Date {
int d, m, y;
public:
Date(int dd, int mm, int yy) : d{ dd }, m{ mm }, y{ yy } {}
};
int main() {
Date today(4, 11, 2024);
Date tomorrow{ today };
}
类似地,类对象默认也可以通过赋值操作拷贝。例如:
Date yesterday = today;
class 和 struct
class
和 struct
都用于定义自定义数据类型,但它们的默认访问权限不同:
-
struct
的成员默认是public
。 -
class
的成员默认是private
。
如果类是一个简单的数据结构,倾向于使用 struct
,如果类是一个具有不变式的真正类型,则倾向于使用 class
。
类成员排序风格推荐:将数据成员放在类的最后,以强调公共接口的函数。这样做可以使代码更加清晰,并且有助于强调类的公共接口。
class Date3 {
public:
Date3(int dd, int mm, int yy);
void add_year(int n); // 增加n年
private:
int d, m, y;
};
构造函数
class Date {
int d, m, y;
public:
Date(int dd, int mm, int yy) : d{ dd }, m{ mm }, y{ yy } {}
};
class Date2 {
int d, m, y;
public:
Date2(int dd=0, int mm=0, int yy=0) : d{ dd }, m{ mm }, y{ yy } {}
};
int main() {
Date today = Date(4, 11, 2024);
Date xmas(4, 11, 2024);
Date realease{ 4, 11, 2024 };
Date bad_date_1; // 错误,缺少初始值(由于定义了构造函数,默认构造函数不再提供)
Date bad_date_2(4, 11); // 错误,漏掉第三个参数
Date bad_date_3{ 4, 11 }; // 错误,漏掉第三个参数
Date2 good_date_1;
Date2 good_date_2(4, 11);
Date2 good_date_3{ 4, 11 };
}
构造函数可提供默认参数。
explicit 构造函数
如果不希望被隐式转换,则使用 explicit
修饰构造函数。
默认情况下,应该将单参数的构造函数声明为 explicit
。
class X {
int x;
public:
X(int x) : x(x) {}
};
class XX {
int x;
public:
explicit XX(int x) : x(x) {}
};
void f(X x) {}
void ff(XX x) {}
int main() {
X x = 1; // OK
XX xx = 1; // Error
X x1 = { 1 }; // OK
XX xx1 = { 1 }; // Error
f(1); // OK
ff(1); // Error
f({ 1 }) // OK
ff({ 1 }); // Error
X x2 = X(1); // OK
XX xx2 = XX(1); // OK
X x3{ 1 }; // OK
XX xx3{ 1 }; // OK
f(X{ 1 }); // OK
ff(XX{ 1 }); // OK
}
注意,explicit 构造函数定义是不要重复关键字 explicit:
class XX {
int x;
public:
explicit XX(int x);
};
explicit XX::XX(int x) : x(x) {}; // 错误
XX::XX(int x) : x(x) {}; // 正确
类内初始化器
当一个类拥有多个构造函数时,成员变量的初始化可能会变得重复和繁琐。为了解决这个问题,C++11 引入了类内初始化器,允许在成员声明时直接初始化成员变量。
class Date {
int d; // today.d
int m; // today.m
int y; // today.y
public:
// 构造函数:天,月,年
Date(int, int, int);
// 构造函数:天,月,当前年份
Date(int, int);
// 构造函数:天,当前月份和年份
Date(int);
// 默认构造函数:today
Date();
// 构造函数:字符串表示的日期
Date(const char*);
// 私有成员函数,检查日期是否合法
void checkDateValidity();
};
这样下面的代码:
// 单参数构造函数实现
Date::Date(int dd) : d(dd) {
// 检查Date是否合法
}
等同于:
// 单参数构造函数实现,等同于
Date::Date(int dd) : d(dd), m(today.m), y(today.y) {
// 检查Date是否合法
}
可变性
常量成员函数
在成员函数的参数列表使用 const
指出该函数不会修改对象的状态。
class MyClass {
public:
// 构造函数
MyClass(int value) : data(value) {}
// 一个不会修改对象状态的成员函数
void display() const {
std::cout << "Data: " << data << std::endl;
}
private:
int data;
};
int main() {
MyClass obj(10);
obj.display(); // 调用不会修改对象状态的成员函数
return 0;
}
物理常量性和逻辑常量性
物理常量性指的是成员函数实际上不修改对象的任何成员变量。
逻辑常量性指的是成员函数从逻辑上看起来不改变对象的状态,尽管它可能更新了一些用户无法直接观察到的内部细节。
class Date {
public:
// 返回日期的字符串表示,逻辑上不改变Date对象的状态
string string_rep() const;
private:
bool cache_valid;
string cache;
void compute_cache_value() const; // 计算并更新缓存
};
在这个类中,有一个成员函数 string_rep()
,它返回一个表示日期的字符串。由于构造这个字符串表示可能非常耗时,类内部保存了一个字符串的缓存。当用户多次请求字符串表示时,可以直接返回缓存的字符串,除非 Date
对象的值已经改变。这种设计意味着 string_rep()
函数需要在某些情况下更新成员变量 cache
和 cache_valid
,即使从用户的角度来看,这个函数并没有改变 Date
对象的状态。
因此,string_rep()
函数应该被声明为 const
成员函数,因为它在逻辑上不改变对象的状态。但是,由于它需要更新缓存,这就需要一种方法来在 const
成员函数中修改成员变量。(见下一小节)
mutable
可以将类成员定义为 mutable,表示即使是在 const 对象中,也可以修改此成员。
#include <iostream>
#include <string>
using namespace std;
class Date {
public:
// 返回日期的字符串表示,逻辑上不改变Date对象的状态
string string_rep() const;
private:
mutable bool cache_valid;
mutable string cache;
int day, month, year;
void compute_cache_value() const; // 计算并更新缓存
};
// 假设string_rep()的实现如下:
string Date::string_rep() const {
if (!cache_valid) {
// 由于这是一个const成员函数,我们不能直接修改成员变量
// 但是我们需要更新缓存,所以我们需要一种方法来修改cache和cache_valid
compute_cache_value(); // 更新缓存
}
return cache;
}
// compute_cache_value()的实现可能如下:
void Date::compute_cache_value() const {
// 计算缓存值的逻辑
// ...
cache = to_string(day) + "/" + to_string(month) + "/" + to_string(year);
cache_valid = true;
}
间接访问实现可变性
此外,还可以将需要改变的数据放在一个独立对象中,通过指针间接访问它。
#include <string>
struct cache {
bool valid;
std::string rep;
};
class Date {
public:
// 返回日期的字符串表示,逻辑上不改变Date对象的状态
std::string string_rep() const;
private:
cache* c; // 指向cache结构体的指针
// 构造函数
Date() : c(new cache) {}
// 析构函数
~Date() {
delete c;
}
// 计算缓存值的函数,可以修改cache结构体的内容
void compute_cache_value() const {
// 假设这里是计算日期字符串表示的逻辑
c->rep = "some date representation";
c->valid = true;
}
};
// string_rep()函数的实现
std::string Date::string_rep() const {
if (!c->valid) {
compute_cache_value(); // 计算缓存值
}
return c->rep; // 返回缓存的字符串表示
}
注意,const 只是不能修改成员,这里对应的是不能修改指针指向其他值,但是可以修改其指向的对象。
自引用
想要实现链式调用,可以在执行操作后,返回自身(返回值是 T&
,通过 *this
返回)。
class Date {
public:
Date& add_year(int n); // 增加n年
Date& add_month(int n); // 增加n个月
Date& add_day(int n); // 增加n天
};
Date& Date::add_year(int n) {
if(this->d == 29 && this->m == 2 && !leapyear(this->y + n)) {
this->d = 1;
this->m = 3;
}
this->y += n;
return *this;
}
在非 static
成员函数中,关键字 this
是指向调用它的对象的指针。在类 X
的非 const
成员函数中,this
的类型是 X*
。但是,this
被当作一个右值,因此我们无法获得 this
的地址或给它赋值。在类 X
的 const
成员函数中,this
的类型是 const X*
,以防止修改对象。
static 成员
- static 成员是类的一部分,而不是某个类对象的一部分。
- static 成员只有唯一副本。
- static 函数只能访问 static 成员。
- static 成员通过类名访问,如
X::
。 - 如果使用了 static 函数或数据成员,就必须在某处定义它。在 static 成员的定义中,不要重复关键字 static。
- 在多线程代码中,static 数据成员需要某种锁机制或访问规则来避免竞争条件。
成员类型
类型和类型别名也可以作为类的成员:
#include <iostream>
#include <string>
// 外部类
class OuterClass {
private:
int privateOuterVar = 10; // 外部类的私有成员
static int privateStaticOuterVar; // 外部类的静态私有成员
public:
int publicOuterVar = 20; // 外部类的公共成员
static int publicStaticOuterVar; // 外部类的静态公共成员
// 内部类
class InnerClass {
private:
int privateInnerVar = 30; // 内部类的私有成员
public:
int publicInnerVar = 40; // 内部类的公共成员
// 内部类的成员函数,访问外部类的成员
void accessOuterMembers() {
// 内部类可以访问外部类的所有静态成员
std::cout << "Accessing OuterClass public var: " << publicOuterVar << std::endl; // 错误
std::cout << "Accessing OuterClass private var: " << privateOuterVar << std::endl; // 错误
std::cout << "Accessing OuterClass public static var: " << publicStaticOuterVar << std::endl; // 正确
std::cout << "Accessing OuterClass private static var: " << privateStaticOuterVar << std::endl; // 正确
}
void accessOuterMembersByObj(OuterClass& outer) {
// 内部类可以通过外部类对象访问外部类的公共成员和私有成员
std::cout << "Accessing OuterClass public var: " << outer.publicOuterVar << std::endl; // 正确
std::cout << "Accessing OuterClass private var: " << outer.privateOuterVar << std::endl; // 正确
}
};
// 外部类的成员函数,访问内部类的成员
void accessInnerMembers(InnerClass& inner) {
// 外部类不能直接访问内部类的私有成员
std::cout << "Accessing InnerClass public var: " << inner.publicInnerVar << std::endl;
// 以下访问是错误的,因为外部类不能访问内部类的私有成员
std::cout << "Accessing InnerClass private var: " << inner.privateInnerVar << std::endl;
}
};
总结:
-
内部类可以访问:
- 不通过对象:外部类的静态成员
- 通过对象:外部类的所有成员(公有 + 私有)
-
外部类可访问内部类的
- 通过对象:公有成员
具体类
具体类:一个类的表示的是其定义的一部分
抽象类:为多种实现提供一个公共的接口
相关函数:
-
成员函数
-
辅助函数
- 可放在类的同一个命名空间中,以表示其关联关系
重载运算符
// 相等判定运算符
inline bool operator==(const Date& a, const Date& b) {
return a.day() == b.day() && a.month() == b.month() && a.year() == b.year();
}
// 自增运算符
Date& operator++(Date& d) {
return d.add_day(1);
}
// 自减运算符
Date& operator--(Date& d) {
return d.add_day(-1);
}
// 加法赋值运算符
Date& operator+=(Date& d, int n) {
return d.add_day(n);
}
// 输出运算符
ostream& operator<<(ostream& os, const Date& d) {
os << d.string_rep(); // 假设string_rep()返回日期的字符串表示
return os;
}
// 输入运算符
istream& operator>>(istream& is, Date& d) {
// 假设Date有一个接受字符串并解析为日期的构造函数
string s;
is >> s;
d = Date(s); // 将字符串解析为Date对象
return is;
}