C++笔记

一、 概述

C语言往往是程序员学习的第一门高级语言,C具有直接面向底层、接近硬件、性能优异的特点。然而在实际开发中,其一些局限性也逐渐显现:

  • 面向对象支持有限:C本身不能原生的支持面向对象的编程范式。但是可以通过结构体、函数指针、宏定义实现类似于C++的面向对象特性。 详细方法可参见此处
  • 类型不安全:C是弱类型语言,包含大量的隐式转换和强大而自由的指针,这两者共同导致了C的类型不安全。C不会进行编译时的严格类型检查,void*可以任意的转换,缺乏数组的边界检查。
  • 资源管理复杂:C的内存需要通过malloc/free手动控制,每个异常的状态都要完整的手动释放所有资源。非常容易导致内存泄漏,需要使用valgrind等工具检查情况。
  • 代码复用困难:C对于不同数据结构实现相同功能的函数需要反复创建。使用宏定义的泛型难以阅读和调试。
  • 错误处理机制混乱:C中存在多种错误处理机制,有的之间返回错误码,有的通过全局的errno管理错误码,错误码需要特别去注意含义是什么。
  • 函数作用域污染C的全局变量之间的作用域没有隔离的机制,导致大型项目容易出现冲突,需要添加前缀管理。

为了解决这些问题,C++使用了一系列的设计:

  • 面向对象(类)
  • 类型安全(STL容器、引用、智能指针、类型转换)
  • RAII和资源管理(构造函数、析构函数、移动语义、035法则)
  • 泛型编程(模板、特化、概念)
  • 异常机制(try、catch、throw)
  • 命名空间(类命名空间和namespace)
  • 其他现代特性(自动类型推导、范围for循环、常量表达式、lamda表达式)

二、具体介绍

1. 类和面向对象

1.1 面向对象的基本思想

面向对象是一种将程序组织为对象的编程范式,每个对象具有自己数据结构(字段、成员变量)和操作数据的方法(方法、成员函数)。和面向过程的模式相比,面向对象的范式里数据和方法是紧密相关,构成了一个逻辑上功能更清晰的整体。

1.2 面向对象的特性

类是实现面向对象的重要方式,类相当于一个模板,类对象相当于模板的一个具体的实例。面向对象的模式通过类实现了封装、继承、多态的特性。

1.2.1 封装

封装是指将类将类内的成员(数据和操作数据的方法)封装起来,仅能通过类提供的接口(公共的方法)来实现数据的访问和修改。对类内的成员,C++使用访问控制关键字:public, protected, private 控制类内部的成员变量和成员函数对外的可见性实现了封装。public:完全可见,对所有代码开放;protected:对本类和继承类可访问,private:完全私有,仅本类可访问、其他类不可访问。

C的封装:在C中,类似的控制可见性的方法是将头文件和实现文件分开,对外开放的接口在头文件中,私有函数在实现文件中并使用static关键字控制它的作用域在当前实现文件内。这种访问控制是面向过程的、文件的封装。

友元:C++中存在着特殊的方法可以打破封装的限制,友元(Friend)。在类中需要声明友元函数或者友元类,这样子友元函数和友元类就可以访问类私有的成员。友元是单向声明传递的。

1.2.2 继承

继承是C++实现代码复用的重要方式,通过继承子类(继承类)可以获得父类(基类)的属性(成员变量)和方法(成员方法)。

1.2.3 多态

多态是C++实现父类和子类的同一方法具有不同实现的机制。具有编译时多态(重载,overload)和运行时多态(重写,overriding)两种。

1.3 成员关键字

静态成员:static 成员变量和函数。

const 成员函数:概念、意义(承诺不修改对象状态)和用法。 mutable关键字。

1.4 类设计的SOLID原则

面向对象的类在设计时需要遵守几个原则,SOLID原则: S - 单一职责原则:每个类只有一个职责。避免类太复杂降低可读性 O - 开闭原则:对扩展开放,对修改关闭。避免反复的修改之前已有的代码 L - 里氏替换原则:子类应该能够替换父类而不影响程序正确性 I - 接口隔离原则: D - 依赖倒置原则:

2. RAII原则、035法则

2.1 RAII原则

2.2 构造函数、析构函数、035法则

构造函数进阶: 初始化列表:为什么它比在构造函数体内赋值更高效(尤其是对于const成员和引用成员)。 委托构造函数(C++11)。 explicit 关键字:防止隐式转换。

移动语义与面向对象:如何为自定义类实现移动构造函数和移动赋值运算符,这是现代C++性能优化的关键。

构造析构函数包括:构造函数、拷贝构造函数、拷贝构造运算符、移动构造函数、移动构造运算符

零法则(Rule of Zero):理想的状况是让类不需要自己定义析构函数/拷贝/移动操作,而是依赖智能指针等成员自动处理,从而更安全简单。

三五法则(Rule of Five):如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,那么它很可能也需要自定义移动构造函数和移动赋值运算符(反之亦然)。这是资源管理类的核心准则。

3. 泛型编程和模板

泛型 模板 特化 概念

4. 异常处理

异常安全保证: 基本保证:发生异常时,程序处于有效状态,无资源泄漏。 强保证:操作要么成功,要么失败并完全回滚(事务语义)。 不抛掷保证:承诺绝不抛出异常(如析构函数)。 不要从析构函数中抛出异常:这是一个重要准则,否则可能导致程序直接终止。 自定义异常类:如何从 std::exception 继承来创建自己的异常类型。

5. 容器、引用、智能指针、强制类型转换

STL容器的部分参见另一个专门介绍STL(Standard Template Library)的笔记 C++ STL笔记

引用是C++对C中的指针进行的封装,在具体实现中常以指针常量的方式实现。指针是一个变量的别名,必须在一开始定义时就进行初始化,一旦确定以后就无法更改其指向的变量(void * const ptr)一切调用都和使用变量一样。

智能指针是为了解决C中裸指针导致的各种内存泄露问题而产生的一种对裸指针进行封装的类,参见 C++智能指针。有独占指针、共享指针和弱指针三种类型,分别用来阐述指针所指向资源的占有情况——独占的持有,只能移动构造;共同持有,通过引用计数释放;不持有,而是只读的观测内容,解决循环计数问题。

强制类型转换 staticcast dynamiccast constcast

作用域和命名空间

C++的作用域在C的基础上添加了两个差异,一个是类作用域的概念,另一个是命名空间的概念。详细内容可以参见这个博客 C C++作用域

auto自动推断

范围for循环

内联函数inline

常量表达式constexpr

Lamda表达式

Lamda表达式是一种临时的内连的函数,用于快速的定义和使用小型的函数对象,形式为:

[捕获列表](参数列表)-> 返回类型{
    函数体
}

捕获列表是Lamda函数可以访问的外部变量,分为以下几种:

  • 值捕获 [=] , 默认是创建cost的副本,可以使用mutable允许修改捕获值
  • 引用捕获 [&],捕获的是引用,修改是可见的
  • 显示捕获(给出捕获的变量列表和捕获方式)
  • 初始化捕获
  • this指针捕获(获得类成员)

参数列表和普通函数的参数列表一致

返回类型用->的方式指定Lamda函数的返回值和类型等信息,默认自动推导

函数体是要执行的函数