类的基本思想:
- 数据抽象:将接口与实现相分类
- 封装:只能用接口,无法访问实现
在成员函数内部,可以直接使用该函数的对象的成员,而无须通过成员访问符。
编译器在实现时,实际上加入了隐式的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对象构造过程中对其进行写值。
默认构造函数:由编译器合成的默认构造函数,按照如下规则进行初始化
- 如果存在类内的初始值,用它来初始化对应成员
- 否则默认初始化该成员
注意:只有未声明任何构造函数时,编译器才会生成默认构造函数
需要注意的是,对于局部的内置类型或复合类型变量(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属性的三种方式:
- 定义在类内部,
- 显示定义inline在相同头文件中
- 声明时就声明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文件。无法知道具体的实现)