0%

C++学习笔记

指针和引用

指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值(这里是在说实参指针本身的地址值不会变)。

而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(int &a的形式)。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

简单一点可以这么想,如果不用引用的话,被传递的参数本身是不能被修改的,即使你传递的是指针,也只能修改指针指向的内容,不能修改指针本身。如果要修改当前被传递的参数的话,要么再加一级指针,要么用引用。

  • 指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
  • 指针是一个实体,而引用仅是个别名;
  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
  • 引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有的,前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
  • 引用不能为空,指针可以为空;
  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)。

Enum&Union

Java 的枚举比较复杂,里面还可以包含方法和成员变量。C/C++ 中的枚举比较纯粹,就是一些特定整数值的集合。在 C++ 中应避免使用 C-style 的枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Deprecated style
enum Fruit {
NONE = 0,
APPLE = 1,
};
// APPLE is visiable

// Suggested style
enum class Fruit {
NONE = 0,
APPLE = 1,
};
// APPLE is invisiable, you need to reference it as Fruit::APPLE.

还可以指定 enum 的 underlying type, 即底层实现使用什么整数类型来存储它:

1
2
3
enum Fruit: char {
// ...
};

Union 的主要使用场景是使用同一个类型存储 A 或者 B 类型的数据,二者不会共存,也可以扩展到多类型:

1
2
3
4
5
6
7
8
9
union test {
int32_t mark; // 4 bytes
uint16_t num[2]; // 4 bytes
uint8_t score; // 1 byte
}; // 总共占用 4 个字节

// use it
union test t;
printf("%d", t.mark);

友元函数和友元类

  • C++ 中引入友元函数,是为在该类中提供一个对外访问的窗口,在类中声明该函数可以直接访问类中的 private 或者 protected 成员。
  • 友元函数的声明可以放在类的私有部分,也可放在共有部分。友元函数的定义在类体外实现,不需要加类限定。
  • 一个类中的成员函数可以是另外一个类的友元函数(友元成员函数),而且一个函数可以是多个类友元函数。
  • 友元函数可以访问类中的私有成员和其他数据,但是访问不可直接使用数据成员,需要通过对对象进行引用。
  • 友元函数在调用上同一般函数一样,不必通过对对象进行引用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Coor {
    public:
    Coor(int x, int y);
    friend void printXY(Coor &coor, int x);
    private:
    int m_iX;
    int m_iY;
    }

    void printXY(Coor &coor, int x) {
    coor.m_iX = x;
    }

    int main(int argc, char const *argv[])
    {
    Coor coor(3, 5);
    printXY(coor);
    return 0;
    }
  • 友元成员函数:当用到友元成员函数时,需注意友元声明和友元定义之间的相互依赖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Coor;  // 当用到友元成员函数时,需注意友元声明与友元定义之间的互相依赖。这是类Coor的声明
    class Draw {
    public:
    // 引用的Coor必须事先声明或者定义
    void run(Coor &coor);
    }

    class Coor {
    public:
    Coor(int x, int y);
    friend void Draw:run(Coor &coor);
    private:
    int m_iX;
    int m_iY;
    }

    void Draw:run(Coor &coor) { }

    int main(int argc, char const *argv[])
    {
    Coor coor(3, 5);
    printXY(coor);
    return 0;
    }
  • 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息,定义友元类的语句格式如下: friend class 类名;
  • 友元关系不能被继承。
  • 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元,要看类中是否有相应的声明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class A {
    public:
    friend class C; // 这是友元类的声明
    private:
    int data;
    };

    class C {
    public:
    // 引用的 A 必须事先定义或声明
    void setShow(int x, A &a) { }
    };

    int main(void) {
    class A a;
    class C c;
    c.setShow(1, a);
    return 0;
    }

类型转换

旧有 C 风格的括号强制类型转换不再推荐使用。

static_cast

1
static_cast<new_type>(expression)

static_cast 强制转换只会在编译时检查,没有运行时类型检查来保证转换的安全性。

dynamic_cast

1
dynamic_cast<new_type>(expression)

new_type 必须是指针或者引用。如果转型失败会返回 null(转型对象为指针时)或抛出异常(转型对象为引用时)。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。

reinterpret_cast

reinterpret_cast 用来做指针类型的转换,或者整数与指针之间的转换。实际上什么都没做,只是改变了我们对于指针指向的内容的理解。

const_cast

1
const_cast<new_type>(expression)

new_type 必须是指针或者引用,const_cast 用于去除或添加对象的 const 属性。

智能指针类型转换

std::unique_ptr 可以转换成 std::shared_ptr,反之不行:

1
2
3
4
5
std::unique_ptr<A> makeA();
std::shared_ptr<A> a = makeA();

std::unique_ptr<A> uniq_a = makeA();
std::shared_ptr<A> shared_a = std::move(uniq_a);

std::static_pointer_cast, std::dynamic_pointer_cast, std::reinterpret_pointer_cast, std::const_pointer_cast 是类型转换操作符的 shared_ptr 版本:

1
2
std::shared_ptr<Fruit> fruit = makeFruit();
std::shared_ptr<Apple> apple = std::static_pointer_cast<Apple>(fruit);

智能指针

首先看看创建对象的方式:

1
2
3
4
5
// 内存分配到栈中,C++自动调用构造函数和析构函数
ClassName obj(param);

// 内存分配到堆中,返回指针,需要使用 delete 操作服释放
ClassName *obj = new ClassName(param);

智能指针是相对于原始指针而言的,看看两者用法的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void UseRawPointer()
{
// Using a raw pointer -- not recommended.
Song* pSong = new Song(L"Nothing on You", L"Bruno Mars");
// use it...
delete pSong; // Don't forget to delete!
}

void UseSmartPointer()
{
// Declare a smart pointer on stack and pass it the raw pointer.
unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));
// use it...
} // song2 is deleted automatically here.

智能指针可以这样理解:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写 delete 语句删除指针指向的内存空间。

STL一共提供了四种智能指针: auto_ptr(弃用), unique_ptr(基本上就是对 new 和 delete 的简单封装), shared_ptr(在 unique_ptr 基础上增加了引用计数的功能,它析构时会将引用计数 -1,直到 0 时将内部数据回收) 和 weak_ptr(对 shared_ptr 管理对象的弱引用)。

所有的智能指针类都有一个 explicit 构造函数,以指针作为参数,因此不能自动将指针转换为智能指针对象,必须显式调用。

  • 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。可以尽量避免 shared_ptr 复制,移动不用增加引用计数,性能比复制更好。
  • 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。因为 C++ 的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的。所以内存上没有任何的额外消耗,性能是最优的。

unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。所以,unique_ptr 不支持复制和赋值,只支持移动。unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权:

1
2
3
4
5
auto w = std::make_unique<Widget>();
auto w2 = w; // 编译错误

auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存,因此 shared_ptr 支持复制。shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。同时,shared_ptr 也支持移动,从语义上来看,移动指的是所有权的传递。

1
2
3
4
5
6
7
8
9
auto w = std::make_shared<Widget>();
{
auto w2 = w;
cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1

auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此时 w 等于 nullptr,w2.use_count() 等于 1

泛型

泛型方法

Java:

1
2
3
public <T> List<T> array2List(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}

C++:

1
2
3
4
template <typename T>
std::vector<T> array2List(const T* a, int64_t size) {
return std::vector<T>(a, a + size);
}

泛型类

Java:

1
2
3
4
public interface List<E> {
void add(E e);
Iterrator<E> iterator();
}

C++:

1
2
3
4
5
6
7
template <typename E>
class List {
public
virtual ~List() = default;
virtual void add(E e) = 0;
virtual Iterator<E> iterator() const = 0;
}

容器

  • std::array: 编译时期确定长度的定长数字
  • std::vector: 变长数组
  • std::list: 相当于 LinkedList
  • std::forward_list: 单向链表
  • std::unordered_map: 相当于 HashMap
  • std::map: 相当于 TreeMap
  • std::stack: 栈
  • std::queue: 队列
  • std::deque: 双向队列
  • std::priority_queue: 优先级队列
  • std::unordered_set: 相当于 HashSet
  • std::set: 相当于 TreeSet
  • std::string: char 数组,为提供 encoding 相关的功能

迭代器

  • 正向迭代器: std::容器类名<item_type>::iterator
  • 常量正向迭代器: std::容器类名<item_type>::const_iterator
  • 反向迭代器: std::容器类名<item_type>::reverse_iterator
  • 常量反向迭代器: std::容器类名<item_type>::const_reverse_iterator

std::vector

注意 std::vector<bool> 内部其实是个 bitmap 而不是一个数组,如果想使用 bool 数组可以使用 std::unique_ptr<bool[]>

  • size: 容器当前拥有的元素个数。
  • capacity: 容器在必须分配新存储空间前可以存储的元素总数,即预分配存储空间的大小。

resize 和 reserve 的区别:

  • resize(n): 修改容器 size 为 n, 至于是否影响 capacity 取决于调整后容器 size 是否大于 capacity。如果 n 小于容器的当前的 size 则删除多出来的元素。resize(n,t) 会将所有新添加的元素初始化。
  • reserve(n): 若容器的 capacity 小于 n 则重新分配内存空间,使得 capacity 等于 n, 如果 capacity >= n 则 capacity 无变化。

由此可见容器调用 resize() 函数后,所有的空间都已经初始化了,所以可以直接访问。而 reserve() 函数预分配出的空间没有被初始化,所以不可访问。因此可以考虑调用 reserve 方法提前申请内存空间来提高性能,避免后续重新申请新的内存空间。

线程与并发

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <thread>
#include "log.h"

void proc(int i) {
LOG_I("LLL", "子线程: %d", i);
}

void test() {
LOG_I("LLL", "----主线程----");
int a = 99;
// 创建并执行线程
auto th = std::make_unique<std::thread>(proc, a);
// 得到 pthread_t
auto t = th->native_handle();
// 设置线程名
pthread_setname_np(t, "子线程1");
// 设置优先级 pthread_setschedparam
th->join();
LOG_I("LLL", "----主线程----");
}

join() 和 detach() 二者都只能调用一次,使用 joinable() 方法判断,其区别:

  • join: 主调线程等待被调线程终止,主调线程回收被调线程资源,并继续运行。
  • detach: 主调线程继续运行,被调线程驻留后台执行,主调线程无法再取得被调线程的控制权,由运行时库负责清理被调线程的资源。

在类中声明成员变量 std::mutex 的时候通常会用 mutable 修饰,这是因为如果需要对外提供 const 成员方法,访问被 mutex 保护的成员时,需要加/解锁,这时需要对 mutex 进行修改。而 mutable 修饰符允许在 const 上下文中修改一个成员变量。

std::mutex

1
2
3
4
5
6
7
8
// 调用线程去锁住该互斥量,如果已经被其他线程锁住了,则阻塞,如果已经被当前线程锁住了,则死锁
lock()

// 释放对互斥量的所有权
unlock()

// 尝试锁住互斥量,如果已经被其他线程锁住了,则返回 false, 如果已经被当前线程锁住了,则死锁
try_lock()

std::recursive_mutex

std::mutex 不同的是,它允许同一个线程对互斥量多次上锁,即递归上锁(可重入?),来获得对互斥量的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同的 unlock() 方法。

std::timed_mutex

std::mutex 多了两个成员函数:

  • try_lock_for: 接受一个时间范围,在一段时间内没获得锁则会被阻塞住。
  • try_lock_until: 接受一个时间点,在指定时间点到来后没获得锁则会被阻塞住。

std::recursive_timed_mutex

顾名思义,递归型的 std::timed_mutex 锁。

std::lock_guard

声明一个局部的 std::lock_guard 对象,在其构造函数中进行加锁,在其析构函数中进行解锁。创建即加锁,作用域结束自动解锁。从而可以使用其替代 lock() 与 unlock() 过程。

需要互斥访问共享资源的那段代码称为临界区,临界区范围应该尽可能的小,可以通过使用 {} 来调整作用域范围,使得互斥量在合适的地方被解锁。

1
2
3
4
5
{
std::lock_guard<std::mutex> lock(mutex);
// lock here
// auto unlock
}

std::unique_lock

unique_lock 类似于 lock_guard 但用法更加丰富,同时支持 lock_guard 的原有功能。使用 lock_guard 后不能手动 lock() 和 unlock(),而 unique_lock 则可以手动控制,通过第二个参数控制初始化时是否加锁。

读写锁

可以使用 std::shared_lockstd::unique_lock 实现读写锁。

atomic/volatile

1
2
volatile int state1;
volatile std::atomic<int> state2;

关键字&符号

default

C++ 的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符,如果没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。在 C++11 标准引入了一个 default 关键字,只需在特殊成员函数声明后加上 =default 即可为其自动生成函数体。

1
2
3
4
5
6
7
8
9
10
class Person {
private:
int age;
public:
Person() = default;
Person(int age) : age(age) {};
void Print() {
std::cout << "Hello Person!" << "\n";
}
};

delete

C++11 标准引入了一个 delete 关键字,当在函数声明后上 =delete 就可将该函数禁用。

explicit

  • 此关键字只能对用户自己定义的对象起作用,不对默认构造函数起作用
  • 此关键字只能够修饰构造函数,而且构造函数的参数只能有一个。
  • 按默认规定,只用传一个参数的构造函数也定义了一个隐式转换。比如说使用 string s = "hearing"; 的过程中实际上隐式调用了 string s("hearing");
  • 使用explicit声明后,则表明构造函数只能显式调用.

const

const 是修饰其左边类型的,除非左边没有类型了,则修饰右边紧挨着的类型,如 const A*A const* 等价。

1
2
3
4
5
6
7
// 常指针。pt不是“只读”的,而 pt 指向的数据不可更改,它指向常量。因此常指针的作用是保护指向的数据不被更改。
const int* pt;
// 指针常量。ps是“只读”的,不能改变指针本身的值,但指向的数据却是能更改的。
int* const ps;
// 常指针常量。指针不能改变,指针指向的值也不能改变。
const int * const pp;
// 实际上不存在引用常量这个说法,因为引用本身就被解释为指针常量,指向不可变,再用const修饰完全没有意义。

const 与函数:

  • const 修饰函数参数: void StringCopy(char *strDestination, const char *strSource), const 只用来修饰输入参数,保护其指向的数据。
  • const 修饰函数返回值: const char* GetString(void), 函数返回值(即指针)指向的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
  • const 成员函数: 在类中定义 int GetCount(void) const, 说明其不会修改数据成员,任何不会修改数据成员的函数都应该声明为const 类型。

const 函数的几点规则:

  • const 对象只能访问 const 成员函数。
  • const 对象的成员是不可修改的,然而 const 对象通过指针维护的对象却是可以修改的。
  • const 成员函数不可以修改对象的数据,不管对象是否具有 const 性质。然而加上 mutable 修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的 const 成员函数是可以修改它的。

constexpr

const 并不能代表常量,它仅仅是对变量的一个修饰,告诉编译器这个变量只能被初始化,且不能被直接修改(实际上可以通过堆栈溢出等方式修改)。

constexpr 可以用来修饰变量、函数、构造函数,一旦被 constexpr 修饰,编译器就可以大胆地将其看成编译时就能得出常量值的表达式去优化。

不要定义 std::string 类型的常量:

1
2
3
4
5
// Don`t do this
constexpr std::string hello = "Hello";

// Do it this way
constexpr char hello[] = "Hello";

std::optional

使用 std::optional 表示一个可能为空的值:

1
2
3
4
5
6
std::optional<Book> makeBook() {
if (failed) {
return std::nullopt;
}
return Book();
}

编码注意事项

  • 宏定义的内容用括号包含。
  • 如果一个类不能被复制或赋值,把拷贝构造函数和赋值操作符禁用掉。
  • 类的所有成员都需要初始化。
  • 头文件中不要有 using namespace。
  • C++ 中的 std::string 只能当作 bytes array 使用,基本上不考虑 encoding 的问题,当成字符串用的时候要小心,很多方法跟其他容器类不一样。如果需要处理 unicode 字符则不要使用它,可以考虑使用 ICU 项目中的 UnicodeString 类型。
  • C++ 中的函数可以不在类里:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 不需要这么写
    class Utils final {
    public:
    Utils() = delete;

    static void Hello();
    }

    // 直接写方法即可
    void Hello();