第一章 基础议题
条款一:仔细区分pointer和references
1.区别
1.指针可以为NULL,引用一定要初始化(初值)。
string& rs; // 错误 引用必须被初始化 string str1("Nancy"); string& rs = str1; // 没问题,rs指向str1 string* ps;// 未初始化的指针,有效,但风险高2.指针可以被重新赋值,指向另一个对象但是引用总是指向它最初获得的哪个对象。
string str1("Nancy"); string str2("Clancy"); string& rs = str1; // rs代表str1 string* ps = &str1; // ps指向str1 rs = str2; // rs仍然代表str1,但是str1的值现在变成了“Clancy” ps = &str2; // ps现在指向str2,str1没有变化2.使用时机
指针:考虑变量“不指向任何对象“的可能型,或者”不同时间指向不同对象“。
引用:
①确定"总是代表某个对象",而且"一旦代表某个对象就不能够再改变"。
②要实现某些操作符时,例如operator[]。
条款二:最好使用C++转型操作符
C++中4个新的类型转换操作符:static_cast、const_cast、dynamic_cast、reinterpret_cast
4个新型转型操作的作用:
static_cast:和C中的类型转换相同。
int a = 10; double b = static_cast<double>(a);const_cast:去除某个对象的常量性。
const int a = 10; int b = const_cast<int>(a); // 去掉a的常量性**dynamic_cast:**用于在继承体系中,“安全的像下转型或跨系转型动作”。
class A { }; class B:public A { }; A* a = new A(); B* b = dynamic_cast<B*>(a);reinterpret_cast:转换“函数指针”类型、但是与编译器平台系相关。
typedef void (*FuncPtr)(); FuncPtr funcArrPtr[10]; int doSomthing(); funcArrPtr[0] = reinterpret_cast<FuncPtr>(&doSomthing);条款三:绝对不要以多态方式处理数组
delete[]对数组执行清理时,编译器只会用“静态类型”计算元素大小和偏移,而不会用“动态类型”做多态。
如果通过基类指针删除派生类对象数组,指针算术会错档,导致未定义行为(通常是堆损坏 / 崩溃)。
#include <iostream> class Base { public: virtual ~Base() { std::cout << "~Base\n"; } }; class Derived : public Base { char dummy[32]; // 让 Derived 比 Base 大很多 ~Derived() { std::cout << "~Derived\n"; } }; int main() { const std::size_t n = 5; Derived* dArray = new Derived[n]; // 生成 5 个 Derived 对象 Base* bPtr = dArray; // 向上转型:语法合法,但埋下炸弹 delete [] bPtr; // ★ 按 Base 的步长去析构 + 释放 }解释:
Derived大小 40 B,Base大小 8 B(仅一个 vptr)。delete[] bPtr从首地址开始,每次偏移 8 B找下一个元素,实际数组步长却是 40 B → 指针迅速指到“半空中”。析构函数因虚表还能正确调到
~Derived,但内存回收时地址已对不上,堆管理器直接 abort。正确姿势:永远不要“多态数组”
// 方法一:用 std::vector<std::unique_ptr<Base>> std::vector<std::unique_ptr<Base>> vec; for (std::size_t i = 0; i < n; ++i) vec.emplace_back(std::make_unique<Derived>()); // 方法二:如果非要用数组,也存指针 std::unique_ptr<Base[]> ptrArray(new Base*[n]); for (std::size_t i = 0; i < n; ++i) ptrArray[i] = new Derived(); // 释放时 for (std::size_t i = 0; i < n; ++i) delete ptrArray[i];小结:“多态对象”请用指针容器/智能指针管理,绝不要把派生类对象直接放进“Base 数组”后再通过 Base* 删除。
条款四:非必要不提供default contructor
只有在“确实需要”时才给类一个默认构造函数(无参 ctor)。
贸然提供T()会让对象处于不确定/无效状态,后续必须靠.init()、open()等二次赋值才能用——这反而增加出错机会并迫使客户端写更多检查代码。
如果对象生来就必须携带有效资源,就让构造函数强制这些资源进来,没有资源就构造失败(抛异常),从而保证“每个对象一出生就有效”。
一、非必要不提供 —— 强制外部传入有效参数
class SerialPort { public: // 没有默认构造!必须给出端口名和波特率 explicit SerialPort(const std::string& port, int baud); ~SerialPort(); void send(const void* data, std::size_t len); std::size_t recv(void* buffer, std::size_t max); private: int fd_; // POSIX 文件描述符 }; SerialPort::SerialPort(const std::string& port, int baud) { fd_ = ::open(port.c_str(), O_RDWR | O_NOCTTY); if (fd_ == -1) throw std::runtime_error("open failed"); // 下面设置 baud、termios … } // 使用端:编译器强制你传入有效参数 SerialPort sp1("/dev/ttyS0", 115200); // OK SerialPort sp2; // ❌ 编译期就报错1.编译器帮你拦下“忘初始化”的对象;
2.构造要么成功(资源立即可用),要么抛异常,不存在“半吊子”状态;
3.后续成员函数无需if (!isOpen()) return;这类重复检查。
二、妥协提供默认构造 —— 带来的额外负担
class SerialPort { public: SerialPort() : fd_(-1) {} // 默认构造:对象处于“无效”态 bool open(const std::string& port, int baud); // 二次初始化 void send(const void* data, std::size_t len); private: int fd_; }; bool SerialPort::open(const std::string& port, int baud) { if (fd_ != -1) return false; // 可能重复打开 fd_ = ::open(port.c_str(), O_RDWR); return fd_ != -1; } // 使用端:很容易写出 bug SerialPort sp; // 1. 对象诞生,但 fd_ == -1 sp.send("hi", 2); // 2. 运行时错误!端口根本没打开问题
1.每个成员函数都要if (fd_ == -1) throw/return;——代码膨胀;
2.客户端必须先调.open()且不忘检查返回值,把本属于类的不变式推给了用户;
3.默认构造后对象处于“无效”期,拷贝、移动语义都要额外处理哨兵值。
三、什么时候“必须”提供默认构造?
1.需要放在标准容器(std::vector<SerialPort> v(10);)
2.需要作为数组元素(SerialPort buf[8];)
小结
“非必要不提供默认构造”就是:
让对象一出生就处于有效、完整、可用的状态;
把“资源获取即初始化”(RAII)做到极致,
把“忘记初始化”这类运行时错误提前到编译期。