C++Primer笔记——第七章:类(上)

类的基本思想:

  • 数据抽象:将接口与实现相分类
  • 封装:只能用接口,无法访问实现

在成员函数内部,可以直接使用该函数的对象的成员,而无须通过成员访问符。
编译器在实现时,实际上加入了隐式的this引用。

与python需要显示指出self不同

  • C++ 的“隐式 this” 追求的是简洁性和一种强烈的封装感,让成员函数感觉自己“身在对象之内”,成员变量触手可及。
  • Python 的“显式 self” 遵循的是**“明确优于隐晦”的核心哲学,追求代码的可读性和无歧义性**,明确区分局部变量和实例属性。

注意,this是一个指向类类型的非常量版本的常量指针

所以我们不能将this绑定到一个常量对象上,
也就是说我们不能在一个常量对象上调用普通的成员函数

this是隐式的,cpp规定,在函数参数列表后加const表示this是一个指向常量的常量指针。
而像这样的使用const成员函数被成为常量成员函数

struct node {
    double f() const ;
};

常量对象,常量对象的引用或指针都只能调用常量成员函数

编译器分两步处理类:

  • 先编译成员的声明
  • 在编译成员函数体

这跟传统的面向过程的代码编写不同,这使得成员函数可以随意使用成员变量
,即使某些成员变量的声明在成员函数的后面

对于类相关的非成员函数,例如

// 此处is类都为引用,因为io类是不能被拷贝的,只能被引用
// 因为io资源是特殊的资源,如果运行拷贝,如何保证的原本与副本之间的同步性?
// 在实现上,IO类的拷贝构造函数和拷贝赋值运算符
// 被声明为 private(在 C++11 之前)或使用 = delete 明确地删除它们(在 C++11 及之后)
istream &read(istream &is, Scles_data &item); 

从概念上属于类的操作,但是实际上不属于类本身
,我们将其声明在与类声明相同的头文件内

构造函数

类的对象只要被创建,就会执行构造函数

构造函数与类名相同,但是没有返回类型。可以有多个构造函数

构造函数不能被声明为const的,因为这和构造函数的用途完全相悖

  • 构造函数 (Constructor) 的唯一使命:创建和初始化一个对象。
    它要把一块原始的、未定义的内存,变成一个状态良好、有初始值的对象。
    这个过程本质上就是一个“”或“修改”的操作。
  • const 成员函数的唯一承诺:不修改对象的状态。
    它保证在调用该函数时,对象的数据成员不会被改变。这是一个只读的操作。

注意,当我们创建一个类的const对象时,直到构造函数完成初始化过程,对象才真正的取得了常量的属性
也就是说,构造函数可以在const对象构造过程中对其进行写值。

默认构造函数:由编译器合成的默认构造函数,按照如下规则进行初始化

  1. 如果存在类内的初始值,用它来初始化对应成员
  2. 否则默认初始化该成员

注意:只有未声明任何构造函数时,编译器才会生成默认构造函数

需要注意的是,对于局部的内置类型或复合类型变量(int、double、指针、数组)等,执行默认初始化的值是未定义

也有可能无法生成默认构造函数,因为有可能包含没有默认构造函数的成员对象,这样编译器就无法生成了

struct Sales_data{
    // 使用 = default 来“复活”默认构造函数
    // = default 还可以用于拷贝构造函数、移动构造函数、析构函数等特殊成员函数。
    Sales_data() = default;

    //这段在冒号与花括号之间的代码为 构造函数初始值列表
    Sales_data(const string &s) : bookNo(s){}
    Sales_data(const string &s,unsigned n, double p):
        bookNo(s),units_sold(n),revenue(p*units_sold){}
    Sales_data(istream &is);
    string isbn() const{return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;

    string bookNo; // 默认构造成空字符串
    unsigned units_sold = 0; // 提供类内初始值
    double revenue = 0.0;// 提供类内初始值
};

函数获得inline属性的三种方式:

  1. 定义在类内部,
  2. 显示定义inline在相同头文件
  3. 声明时就声明inline,然后在cpp文件中实现

而如果是简单的将声明写在头文件内而定义写在cpp文件内,则不会默认为inline

第一种情况

// MyClass.h
class MyClass {
public:
    // 这个函数是 inline 的,因为它的定义在 class {} 内部。
    void doSomething() { 
        // ...
    }
};

第二种情况

// MyClass.h
class MyClass {
public:
    void doSomething(); // 只有声明
};

// 仍然在 MyClass.h 文件中
inline void MyClass::doSomething() {
    // 这个函数是 inline 的,因为显式地用了 inline。
    // ...
}

第三种情况

// MyClass.h
class MyClass {
public:
    inline void doSomething(); // 在声明处标记为 inline
};

// MyClass.cpp
#include "MyClass.h"

// 定义可以不用再写 inline,但这个函数仍然是 inline 的。
// 然而,这种写法有一个巨大的问题!
void MyClass::doSomething() {
    // ...
}

这种写法的陷阱在于:虽然把 doSomething 声明为了 inline,但它的定义却藏在了 .cpp 文件里。
这意味着,当其他 .cpp 文件(比如 main.cpp)包含 MyClass.h 并调用 doSomething 时,
编译器看不到它的函数体,因此无法进行内联展开!
而且,由于它被标记为 inline,编译器可能不会在目标文件中为它生成一个独立的、可供链接的版本,
这可能导致链接错误(“undefined reference”)。

//main.cpp

#include "MyClass.h"
int main() {
    MyClass myClass;
    myClass.doSomething(); // 编译出错


    return 0;
}

正是因为这个原因,inline 函数的定义(而不仅仅是声明)必须放在头文件中

值得注意的是,如果修改为#include "MyClass.cpp"则是可以运行的,
因为include预处理器的作用不过是把全部内容复制粘贴到 #include 指令所在的位置
。这样实际上main.cpp就有了Myclass.cpp的所有内容,实际上就只有一个cpp文件。

但是这严重违反了单一定义规则 (ODR),如果有另一个文件如 other.cpp,它也天真地 #include “MyClass.cpp”
则:

  • 编译 main.cpp 会生成一个包含 MyClass::doSomething 定义的目标文件
  • 编译 other.cpp 也会生成一个包含 MyClass::doSomething 定义的目标文件。
  • 当链接器试图把这两个目标文件链接在一起时,它会发现 MyClass::doSomething 有两个完全相同的定义。
    即使它们是 inline 的,这种 #include .cpp 的方式也会让链接器感到困惑,并最终导致**“多重定义 (multiple definition)”** 的链接错误。

且破坏了接口与实现分类的设计原则

// 如果去除了inline,则会出现多重连接的问题
// MyClass.h
class MyClass {
public:
     void doSomething(); 
};

// MyClass.cpp
#include "MyClass.h"

#include <iostream>

void MyClass::doSomething() {
    std::cout << "Hello World!\n";
}

// main.cpp
#include "MyClass.cpp"
int main() {
    MyClass myClass;
    myClass.doSomething();


    return 0;
}
// other.cpp
#include "MyClass.cpp" 
//也include了MyClass.cpp
// 则将会把MyClass.cpp的内容完全赋值进来,
// 由于不为inline函数,则将会编译出两个MyClass::doSomething实现
// 从而导致了main.cpp编译错误

回到构造函数

构造函数初始值列表

struct Sales_data{
    // 使用 = default 来“复活”默认构造函数
    // = default 还可以用于拷贝构造函数、移动构造函数、析构函数等特殊成员函数。
    Sales_data() = default;

    //这段在冒号与花括号之间的代码为 构造函数初始值列表
    Sales_data(const string &s) : bookNo(s){}
    Sales_data(const string &s,unsigned n, double p):
        bookNo(s),units_sold(n),revenue(p*units_sold){}
    Sales_data(istream &is);
    string isbn() const{return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;

    string bookNo; // 默认构造成空字符串
    unsigned units_sold = 0; // 提供类内初始值
    double revenue = 0.0;// 提供类内初始值
};

如果在构造函数的初始化列表里为一个成员赋值,那么这个值会覆盖掉类内初始值

class Window {
private:
    int width = 800;  // 类内初始值
    int height = 600; // 类内初始值
public:
    // 这个构造函数有明确的定制化需求
    Window(int w, int h) : width(w), height(h) {} 
    // 当调用 Window(1024, 768) 时:
    // width 会被初始化为 1024 (覆盖了 800)
    // height 会被初始化为 768 (覆盖了 600)
};

访问控制与封装

加入public和private

可以有多个private和public,每个访问说明符指明了接下来的成员的访问级别,
其有效范围到下一个访问说明符或者类的结尾处为止

C++ 标准规定了 struct 和 class 之间只有以下两个区别:
a) 默认成员访问权限 (Default Member Access)

  • struct: 成员默认是 public (公开的)。
  • class: 成员默认是 private (私有的)。
    b) 默认继承权限 (Default Inheritance Visibility)
  • struct: 默认是 public 继承。
  • class: 默认是 private 继承。

在使用的时候,一般是

  • 想要定义一个纯粹的数据聚合体 (Data Aggregate) 时,使用 struct
  • 想要创建一个拥有行为和封装状态的真正对象 (Object) 时,使用 class

友元(friend)
类可以允许其他类或者函数访问它的非公有成员

#include <iostream>
#include <string>

class Sales_data {
// 为 Sales_data 的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
// 其他成员及访问说明符与之前一致
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p):
               bookNo(s), units_sold(n), revenue(p*n) { }
    Sales_data(const std::string &s) : bookNo(s) { }
    Sales_data(std::istream&);
    
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data&);

private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

// Sales_data 接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);

其实就算没有在对于头文件内但在类外写想要的声明也可以

// MyClass.h
class MyClass {
private:
    int secret = 42;

public:
    MyClass() = default;
    friend void revealSecret(const MyClass& obj);

    // 注意:头文件里没有在类外部再次声明 revealSecret
};

//friend_functions.cpp
#include "MyClass.h"

// 在这个 .cpp 文件中,我们直接提供了 revealSecret 的定义。
// 这是完全合法的,因为通过 #include "MyClass.h",
// 编译器已经看到了这个函数的声明(就在 class MyClass 内部)。
void revealSecret(const MyClass& obj) {
    // 因为是友元,所以可以访问 private 成员
    std::cout << "The secret is: " << obj.secret << std::endl; 
}

//main.cpp
#include "MyClass.h"

// 同样,我们也需要声明 revealSecret 才能调用它。
// 这个声明从哪里来?从 #include "MyClass.h" 内部的 friend 声明来!
void revealSecret(const MyClass& obj); // 这一行其实是可选的,但写上更清晰

int main() {
    MyClass obj;
    revealSecret(obj); // 调用友元函数
    return 0;
}

在main.cpp中,不写revealSecret函数的声明也可以的,因为这涉及了一个参数相关查找 (Argument-Dependent Lookup, ADL)
的核心概念:
当你调用 revealSecret(obj) 时,其中 obj 的类型是 MyClass。
ADL 规则规定,编译器除了在当前作用域查找 revealSecret 之外,
还会自动地到参数类型 MyClass 所属的命名空间(和类本身)中去查找

所以这样写也可以:

#include "MyClass.h"

int main() {
    MyClass obj;
    // 即使没有前置声明,ADL 也会通过 obj 的类型 MyClass
    // 找到在 MyClass 内部声明的友元函数 revealSecret
    revealSecret(obj); 
    return 0;
}

private 不是绝对的安全保险箱,最终的防线是链接器,如果我们去重新定义一个函数实现,
表面上我们可以通过这个函数来获得所有private的内容,但是在编译后进行链接就会发生与原来的
实现冲突(多重定义)。(而原来的文件也不会提供cpp源代码,只会有h文件和o文件。无法知道具体的实现)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值