说明
基础篇9讲主要学习内存管理、容器、迭代器、异常、C++11/14/17的新语法。
每日打卡更新。
打卡Day1:堆、栈、RAII:C++里该如何管理资源?
- 内存管理:
C++的堆和栈与JAVA中意思是一样的,不同的是,C++中没有垃圾回收这个概念,而是使用RAII管理内存(参考第一部分析构函数部分代码);
栈中的内存,在函数调用结束后,自动释放,不需要手动释放,而且由于栈的LIFO特性,保证了释放内存空间的连续性;
堆内存分配需要使用new关键字,分配内存,并且使用delete关键字手动释放内存。 - 和JAVA语言不同的是,C++的默认都是值定义,只有指针或者引用对象才可以new对象(new的对象才会在堆上面分配内存),不new的对象也会默认调用构造函数,但是内存并不是分配在堆上面,而是分配在栈上面。
打卡Day2:自己动手,实现C++的智能指针
- 一些基础概念学习:
运算符重载:C++中的运算符都可以看做是一个函数,这样就类似于Java中的函数重载。
拷贝构造函数:通过使用另一个同类型的对象来创建一个新对象。
拷贝赋值:重载拷贝赋值运算符。
移动构造函数:构造新的对象后,原有对象资源清空。
移动赋值: 重载赋值运算符。
现代C++中尽量不要使用裸指针(原始指针),最好使用标椎库中智能指针(shared_ptr和unique_ptr)。
根据第一讲的RAII思想,实现shared_ptr和unique_ptr。
shared_ptr的对象使用拷贝操作;
unique_ptr的对象使用移动操作;
引用计数(共享计数):shared_ptr和unique_ptr的最大区别,就是share_ptr多个智能指针同时拥有一个对象,需要一个共享计数,用来标记,是否需要删除这个对象。
第一讲和第二讲的代码如下:
#include <iostream>
using namespace std;
// template 类似于Java中的泛型
template <typename T>
class smart_ptr {
private:
T *ptr_;
public:
// explicit 构造函数关键字;
// ptr_(ptr)表示初始化构造函数的成员变量,这个地方由于不熟悉语法,一开始没有理解。
// C++11起 空指针用nullprt表示,古代C++用0或者NULL
explicit smart_ptr(T *ptr = nullptr) : ptr_(ptr) {
cout << "调用默认构造函数,分配内存;" << endl;
}
//析构函数(delete 对象时,才会调用)
~smart_ptr() {
cout << "调用析构函数,释放内存;" << endl;
delete ptr_;
}
//拷贝构造函数(通过使用另一个同类型的对象来创建一个新对象。)
smart_ptr(smart_ptr<T> &other) {
cout << "调用拷贝构造函数;\n";
ptr_ = other.rlease();
}
//拷贝赋值(覆盖原有的对象)
smart_ptr<T> &operator=(smart_ptr<T> &other) {
cout << "调用拷贝赋值,赋值对象;\n";
smart_ptr(other).swap(*this);
return *this;
}
//移动构造函数
smart_ptr(const smart_ptr &&other) { cout << "调用移动构造函数\n"; }
smart_ptr<T> &operator=(smart_ptr<T> other) {
other.swap(*this);
return *this;
}
void swap(smart_ptr &rhs) {
using std::swap;
swap(ptr_, rhs.ptr_);
}
//将传入引用对象赋值为空,并把该对象赋值给新创建的对象
T *rlease() {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
// const
// 表示该方法外部不能修改,和java不同,在C++中,const可以用来声明成员函数
T *get() const {
cout << "对象地址:" << this << ";get==>ptr value:" << *ptr_ << endl;
return ptr_;
}
// C++中的重载运算符,类似于重载现有的运算符函数。
T *operator->() const { return ptr_; }
T &operator*() const { return *ptr_; }
};
//共享计数
class share_count {
public:
share_count() : count_(1){};
void add_count() { ++count_; }
long reduce_count() { return --count_; }
long get_count() const { return count_; }
private:
long count_;
};
// 程序的主函数
int main() {
string value = "share_ptr";
string *ptr = &value;
smart_ptr<string> ptr1{
ptr}; // C++11起可以用大括号初始化一个对象,调用默认构造函数
// smart_ptr<int> ptr2;
ptr1.get();
// delete ptr1;
smart_ptr<string> ptr3{ptr1}; //调用拷贝构造函数
ptr3.get();
smart_ptr<string> ptr4;
//ptr4=ptr1; //调用拷贝赋值,编译器会报错;
ptr4 = std::move(ptr1); //调用移动构造函数(C++11起)
ptr4.get();
// 设置长度
return 0;
}
验证输出结果:
调用默认构造函数,分配内存;
对象地址:0x7ffffffedf08;get==>ptr value:share_ptr
调用拷贝构造函数;
对象地址:0x7ffffffedf10;get==>ptr value:share_ptr
调用默认构造函数,分配内存;
调用移动构造函数
调用析构函数,释放内存;
对象地址:0x7ffffffedf18;get==>ptr value:share_ptr
调用析构函数,释放内存;
打卡Day3:右值和移动究竟解决了什么问题?
继续理解一些基础概念
- 左值和右值的概念好理解,例如++x是右值,是一个临时变量;一个特殊的概念是亡值,是右符号标记的有值,联系到上一讲的移动构造函数调用就是std::move(ptr1)亡值。
拷贝构造函数和移动构造函数中的参数,一个是左值引用(T&),一个是右值引用(T&&)。
// lvalues_and_rvalues2.cpp
int main()
{
int i, j, *p;
// i 是 lvalue , 7 是prvalue.
i = 7;
// 错误使用: `j * 4` 是一个 prvalue.
7 = i; // C2106
j * 4 = 7; // C2106
// *p 是 lvalue.
*p = i;
// ((i < 3) ? i : j) 是左值
((i < 3) ? i : j) = 7;
// 错误使用: ci is 不能修改的左值
const int ci = 7;
ci = 9; // C3892
}
- 与Java不同的是,C++中的原生类型、枚举、结构、联合、类都是值类型,只有指针和引用是引用类型。
- C++移动的真正含义:
class A {
B b_;
C c_;
};
从实际内存布局的角度,很多语言——如 Java 和 Python——会在 A 对象里放 B 和 C 的指针(虽然这些语言里本身没有指针的概念)。而 C++ 则会直接把 B 和 C 对象放在 A 的内存空间里。这种行为既是优点也是缺点。说它是优点,是因为它保证了内存访问的局域性,而局域性在现代处理器架构上是绝对具有性能优势的。说它是缺点,是因为复制对象的开销大大增加:在 Java 类语言里复制的是指针,在 C++ 里是完整的对象。这就是为什么 C++ 需要移动语义这一优化,而 Java 类语言里则根本不需要这个概念。
上面的一段直接引用。
- 返回一个本地对象意味着这个对象会被拷贝,虽然编译器会做返回值优化,因此,不要返回本地变量引用。
打卡Day4:容器汇编 I:比较简单的若干容器
- 这一讲都是简单的顺序数据结构
vector 类似于java中的Arraylist;
deque 类似于java中的ArrayDeque;
list(双链表)类似于java中的LinkedList;
forward_list是C++11中提供的单链表。
queue和stack没有begin和end成员函数,所以不支持迭代访问。
在C++中有个容器适配器概念,意思是queue和stack支持定义基础容器类型,默认的基础容器类型是deque。 - noexcept保证一个移动构造函数不抛出异常。
如果vector的移动构造函数不能确保不抛出异常,vector 通常会使用拷贝构造函数。这种情况下对于某些自定义类型的拷贝就是灾难。
代码如下:
#include <iostream>
#include <vector>
using namespace std;
class obj1 {
private:
/* data */
public:
obj1() { cout << "obj1 构造函数\n"; };
~obj1() { cout << "obj1 析构函数\n"; }
obj1(const obj1&) { cout << "obj1 拷贝构造函数\n"; }
obj1(obj1&&) { cout << "obj1 移动构造函数\n"; }
};
class obj2 {
private:
/* data */
public:
obj2() { cout << "obj2 构造函数\n"; };
obj2(const obj2&) { cout << "obj2 拷贝构造函数\n"; }
// noexcept 保证提供一个不抛异常的移动构造函数
obj2(obj2&&) noexcept { cout << "obj2 移动构造函数\n"; }
};
int main() {
vector<obj1> v1;
//使用reserve 分配连续内存空间
v1.reserve(2);
//在尾部新构造一个元素
v1.emplace_back();
v1.emplace_back();
//构造第三个对象时,内存不足,分配一个新的内存,并构造第三个对象,
//同时调用拷贝构造函数,复制第一个和第二个对象
v1.emplace_back();
vector<obj2> v2;
v2.reserve(2);
v2.emplace_back();
v2.emplace_back();
// 构造第三个对象时,内存不足,需要分配一个新的内存,构造第三个对象,
//同时调用移动构造函数(因为该函数保证了抛出异常),复制第一个和第二个对象
v2.emplace_back();
}
输出:
#第一次调用v1.emplace_back();
obj1 构造函数
#第二次调用v1.emplace_back();
obj1 构造函数
#第三次次调用v1.emplace_back();
obj1 构造函数
obj1 拷贝构造函数
obj1 拷贝构造函数
#完成后释放旧vector中的两个对象内存
obj1 析构函数
obj1 析构函数
#第一次调用v2.emplace_back();
obj2 构造函数
#第二次调用v2.emplace_back();
obj2 构造函数
#第三次调用v2.emplace_back();
obj2 构造函数
obj2 移动构造函数
obj2 移动构造函数
#完成后释放新vector中的三个对象内存
obj1 析构函数
obj1 析构函数
obj1 析构函数
打卡Day5:容器汇编 II:需要函数对象的容器
熟悉两个概念:
- 函数对象,我的理解是类或者结构重新定义了重载运算符"()",则该类或者结构声明的对象就是函数对象
//less struct
template <class T>
struct less
: binary_function<T, T, bool> {
//重载运算符“()”
bool operator()(const T& x,
const T& y) const
{
return x < y;
}
};
// hash struct
template <class T> struct hash;
template <>
struct hash<int>
: public unary_function<int, size_t> {
size_t operator()(int v) const
noexcept
{
//static_cast类型转换函数,size_t是无符号整数类型
return static_cast<size_t>(v);
}
};
这一讲的容器都会带上这两个函数对象
- priority_queue这种数据结构和stack类似,但是默认带有less函数对象,这样栈顶存储的就是最大数。
- C++中的关联容器( set、map、multiset和 multimap)与Java不同,不是按插入数据的顺序,会默认按key排序。为此,从C++11起定义了无序关联容器(unordered_set,unordered_map,unordered_multiset,unordered_multimap);
- C++ 中的set不允许存在重复的元素,要存储重复的元素,需要使用multiset和 multimap
- 使用数组的话,不建议用C的原始数组,
较大数组可以使用vector,较小的数组可以使用array(C++ 11)
由于容器的输出不方便,测试的时候推荐使用 xeus-cling 在线可视化工具。
xeus-cling online
打卡Day6:异常,用还是不用,这是个问题
- C++标准库里面已经使用了异常,所有肯定会用,最关键的是当异常发生时,要保障不会发生内存泄露,所以要理解RAII,在JAVA中会用GC机制释放内存。
- Google不建议使用异常,有两方面原因,一是历史原因,早期他们的编译器对异常处理不好,所有他们产生了一大堆异常不安全的代码;另一个原因是追求更高的性能(打开和关闭异常时,会产生一些二进制文件大小)
- 第一讲有个“栈展开”,其实就是在函数执行异常之前,调用函数之前所有局部变量的析构函数,释放所有资源。
打卡Day7:迭代器和好用的新for循环
- 迭代器是为了解耦数据结构和算法。先泛型数据容器,然后泛型数据容器的迭代器,最后泛型算法就好写了。本质上是一种将容器的访问高度抽象,Java语言编译器自动处理了这些,它底层的思想也是类似C++的指针。
- 作者列了一些常用迭代器:
Input迭代器,需要重载,->,==,!=运算符;
Output迭代器,需要重载,->,==,!=运算符;;
Forward迭代器:具有input和out迭代器的所有功能;
Bidirectional迭代器,具有Forward迭代器所有功能;
Random Access迭代器;
下面是Input迭代器的实现:
#include <fstream>
#include <iostream>
#include <istream> // std::istream
#include <iterator> // std::input_iterator_tag
#include <string>
using namespace std;
class istream_line_reader {
public:
class iterator {
public:
typedef ptrdiff_t difference_type;
//迭代器指向的对象的值类型
typedef std::string value_type;
//迭代器指向的对象的指针类型
typedef const value_type* pointer;
//迭代器指向的对象的引用类型
typedef const value_type& reference;
//输入迭代器
typedef std::input_iterator_tag iterator_category;
iterator() noexcept : stream_(nullptr) {}
explicit iterator(istream& is) : stream_(&is) { ++*this; }
reference operator*() const noexcept { return line_; }
pointer operator->() const noexcept { return &line_; }
iterator& operator++() {
getline(*stream_, line_);
if (!*stream_) {
stream_ = nullptr;
}
return *this;
}
iterator operator++(int) {
iterator temp(*this);
++*this;
return temp;
}
//迭代器相等比较
bool operator==(const iterator& rhs) const noexcept {
return stream_ == rhs.stream_;
}
//迭代器不等比较
bool operator!=(const iterator& rhs) const noexcept {
return !operator==(rhs);
}
private:
istream* stream_;
string line_;
};
istream_line_reader() noexcept : stream_(nullptr) {}
explicit istream_line_reader(istream& is) noexcept : stream_(&is) {}
iterator begin() { return iterator(*stream_); }
iterator end() const noexcept { return iterator(); }
private:
istream* stream_;
};
int main() {
//读取文件
ifstream ifs{"test.txt"};
for (const string& line : istream_line_reader(ifs)) {
//示例循环体中仅进行简单输出
cout << line << endl;
}
}
打卡Day8:易用性改进 I:自动类型推断和初始化
- 类型推断,Java10 应该也支持了类型推断var;
auto:编译器会根据表达式的类型推断变量类型,不需要手动声明。 - 函数模板参数推导,其实auto的实现规则类似模版参数推导。根据实际调用的实参类型,推导形参类型。
值类型参数推导;
引用类型参数推导;
//定义一个模版函数
template<typename T>
T max(T a, T b)
{
// if b < a then yield a else yield b
return b < a ? a : b;
}
//调用
int const c = 42;
max(i, c); // OK :实际调用mar(int,int)
max(c, c); // OK: 实际调用mar(int,int)
int arr[4];
max(&i, arr); // OK: 实际调用mar(int*,int*)
max(4, 7.2); //ERROR: T 有可能是 int 或 double
std::string s;
foo("hello", s); //ERROR: T 有可能是char const[6] 或者std::string
- 类模板推导
#include <iostream>
using namespace std;
int main() {
//推导,C++17之前的写法 auto pr = make_pair(1, 42);
//这里还用到C++11的大括号初始化写法。
pair pr{1, 2};
}
打卡Day9:易用性改进 II:字面量、静态断言和成员函数说明符
- 字面量:,在Java中,会有一些标准的字面量,例如3.14f表示浮点数。这个字面量是原生支持的,在传统C++(C++11之前)中,也是如此,但是从C++11起开始支持自定义字面量。
#include <iostream>
struct length {
double value;
//单位
enum unit {
metre,
kilometre,
millimetre,
centimetre,
inch,
foot,
yard,
mile,
};
static constexpr double factors[] = {1.0, 1000.0, 1e-3, 1e-2,
0.0254, 0.3048, 0.9144, 1609.344};
explicit length(double v, unit u = metre) { value = v * factors[u]; }
};
//重载字面量运算符(""),自定义字面量_m(注意自定义的必须以_开头)
length operator"" _m(long double v) { return length(v, length::metre); }
//自定义字面量_cm
length operator"" _cm(long double v) { return length(v, length::centimetre); }
length operator+(length lhs, length rhs) {
return length(lhs.value + rhs.value);
}
int main() {
length lhs = 1.0_cm; //调用运算符 "" _m(1.0)
length rhs = 2.0_m; //调用运算符 "" _cm(2.0)
length sum = lhs + rhs;
std::cout << sum.value << std::endl;
}
- default 和 delete 成员函数
第二讲说过,当定义一个类类型(class,struct)时,开发者不声明构造函数,析构函数,拷贝函数,移动函数,拷贝赋值运算函数,移动赋值运算函数,编译器会自动声明这些函数。
当类中存在用户声明的构造函数时,用户仍可以关键词 default 强制编译器自动生成原本隐式声明的默认构造函数,析构函数,拷贝函数,移动函数,拷贝赋值运算函数,移动赋值运算函数。
#include <iostream> //std::cout
struct A {
int x;
A(int x = 1) : x(x) {
std::cout << "调用A构造函数" << std::endl;
} // 用户定义默认构造函数
};
struct B : A {
// 隐式定义 B::B(),调用 A::A()
};
struct C {
A a;
// 隐式定义 C::C(),调用 A::A()
};
struct D : A {
D(int y) : A(y) {}
// 不会声明 D::D(),因为存在另一构造函数
};
struct E : A {
E(int y) : A(y) {}
E() = default; // 显式预置,调用 A::A()
};
struct F {
int& ref; // 引用成员
const int c; // const 成员
// F::F() 被隐式定义为弃置
};
int main() {
A a;
B b;
C c;
// D d; // 编译错误
E e;
// F f; // 编译错误
}
- override 和 final 关键字
这个两个关键字和Java中意义是一样的。
class A {
public:
virtual void foo();
virtual void bar();
void foobar();
};
class B : public A {
public:
void foo() override; // OK
void bar() override final; // OK
//void foobar() override;
// 非虚函数不能 override
};
class C final : public B {
public:
void foo() override; // OK
//void bar() override;
// final 函数不可 override
};
class D : public C {
// 错误:final 类不可派生
…
};