16. 类

类基础

类的简要概括:

  1. 一个类就是一个用户自定义类型。
  2. 一个类由一组成员构成,最常见的成员类别是数据成员和成员函数。
  3. 成员函数可以定义初始化(创建)、拷贝、移动和清理(析构)等语义。
  4. 对对象使用 .​(点)访问成员,对指针使用 ->​(箭头)访问成员。
  5. 可以为类定义运算符,如 +​、! ​和 &​。
  6. 一个类就是一个包含其成员的名字空间。
  7. public ​成员提供类的接口,private ​成员提供实现细节。
  8. 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;
}

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇