[C++/笔记]C++ Primer 自用学习笔记(持续更新 最新8.21)

目录

说明

碎碎念日志

参考

环境 编译 文件相关

VS2015 设置 快捷键

头文件

stdafx.h

基本语言

基本类型

const  限定符

引用&

auto 类型说明符

decltype 类型指示符

表达式

new和delete

数组和指针

指针

void*指针

指向const对象的指针 (底层const)

const指针 (顶层const)

函数

参数传递

默认实参

动态内存

智能指针

直接管理内存 new和delete

shared_ptr和new结合使用

unique_ptr

weak_ptr

类和数据抽象

类的成员函数

内联函数 inline

this指针

const成员函数

构造函数

重载函数

析构函数

友员friend

static类成员

嵌套类

拷贝控制

拷贝构造函数

重载操作符

右值引用

万能引用

完美转发

移动构造

顺序容器

迭代器

vector

容器遍历

vector自增长原理

顺序容器操作

push_back和emplace_back的区别

push_back的工作原理

emplace_back的工作原理

性能差异和选择建议

顺序容器适配器

关联容器

map,>

set

有序容器自定义顺序

关联容器操作

面向对象编程 OOP

继承

多重继承

虚函数

纯虚函数

访问控制


说明

主要参考C++ Primer 第四版 第五版
可能会有错 欢迎大佬们指正
仅C++基础知识记录 不含任何工作/业务代码

碎碎念日志

7.17 心态崩了 本来这里写的是 突破万字!!✿✿ヽ(°▽°)ノ✿
结果点到代码旁边那个资源绑定 CSDN页面直接拉了 三千多字没了!!!!! WCTD
不过这里的字数 英文算的是字母 6

8.8 开了个学习分享会 补充几个点 右值引用写的更详细了一些 新增万能引用和完美转发的内容 补充通过using 避免隐藏基类同名函数的细节 vector中push_back和emplace_back的区别

8.21 中间这段时间全部在本地用MarkText敲了.md格式的文档 然后直接粘到CSDN来了 可能有部分排版有点乱(特别是换行)

参考

能分类的参考在各个章节里有链接 一些泛用的参考链接放在在这里

C++11特性 学习笔记 知乎

环境 编译 文件相关

VS2015 设置 快捷键

VS2015社区版怎么没有"调试停止时自动关闭控制台"这个选项啊

注释 Ctrl + K + C 取消注释 Ctrl + K + U (手动更改设置) 单行快速注释不需要选中注释内容
Ctrl + W
Ctrl + ←/→
Ctrl + J 代码提示/补全(有没有什么智能插件)
Alt + ↑/↓ 当前行/选中行 上/下移动
Ctrl + Enter 上插空行
Ctrl + Shift + Enter 下插空行

通过指针访问结构的成员时,要手动->, Qt中.运算符自动判断是否需要切换到->运算符
启用展开xx是方便加括号的东西

头文件

头文件中只定义确实必要的东西
不应包含变量或函数的定义 除了类定义 const对象 inline函数
避免同一头文件的多次包含 使用头文件保护符

#ifndef (主要用这种) 优点: 跨平台 兼容性好
缺点: 宏名不要重复 可能产生同名冲突 大型项目编译时间长

#ifndef XXX_H
#define XXX_H
...
#endif // XXX_H

#pragma once (规范说要用这种) 优点: 不用起宏名 没有宏的名字冲突问题 大型项目编译速度快
缺点: 有的编译器不支持 不便于跨平台

#pragma once
// content of this file

参考:

#pragma once与#ifndef的区别_#pragma once和#ifndef区别-CSDN博客

C语言 #pragma once - C语言零基础入门教程-CSDN博客

stdafx.h

生成预编译头文件 简单来说是 提高编译速度 减少重复编译 便于管理维护

一、什么是预编译头?
  所谓预编译头,就是把头文件事先编译成一种二进制的中间格式,供后续的编译过程使用。预编译头物理上与通常的的.obj文件是一样的,但是千万不要把这个中间格式与. o/.obj/.a/.lib的格式混淆,他们是截然不同的!所以预编译头文件的特性和目标文件也不同(尽管他们都属于某种中间文件)。编译入预编译头的.h,.c,.cpp文件在整个编译过程中,只编译一次,如预编译头所涉及的部分不发生改变的话,在随后的编译过程中此部分不重新进行编译。进而大大提高编译速度,并便于对头文件进行管理,也有助于杜绝重复包含问题。——但也有类似的地方的,比如,它们都是编译器之间不兼容的^_^,就是说你不能把VC生成的预编译头拿到GCC上去用。甚至扩展名都不一样,VC的是大家都熟悉的. pch,而GCC的,是.gch。
二、什么时候使用预编译头?
  当大多.c或.cpp文件都需要相同的头文件时。
  当某些代码被大量重复使用时。
  当导入某些不同库都有实现的函数,并产生混乱时。

其中包含了stdio.h
stdafx.h仅适用于支持MFC(微软基础类库Microsoft Foundation Classes)的平台
可以在项目->属性->c/c++->预编译头,进行设置,开启、关闭预编译头

C++开发基础之预编译头文件 stdafx.h的作用-CSDN博客

#include“stdafx.h”详解_include stafx.h-CSDN博客

基本语言

基本类型

1word(字) = 4Byte(字节) = 32bit(位)

typedef signed char        int8_t;  
typedef short              int16_t;  
typedef int                int32_t;        //不同系统长度可能不同  
typedef long long          int64_t;  
typedef unsigned char      uint8_t;  
typedef unsigned short     uint16_t;  
typedef unsigned int       uint32_t;  
typedef unsigned long long uint64_t;  // 不要写long long
typedef unsigned int     size_t;

代码规范: 一般用具体长度的类型 int32_t 代替直接用int 等

字符串字面值
C++中字符串后自带一个空字符'\0'

sizeof("A");    //2
sizeof("A" "B");    //3 相当于"AB\0"
sizeof('A');    //1

const  限定符

常量定义后不可修改 必须初始化 const int i = 10;

顶层const 底层const(第五版)(见指针部分)

顶层const表示指针本身是个常量

底层const表示指针所指对象是一个常量

引用&

引用只是他绑定的对象的别名 必须用与其同类型的对象初始化
引用本身不是一个对象 一旦定义了一个引用就不能绑定到其他对象

const对象 必须用 const引用 指向

const引用 可以 指向 非const对象
const引用 可以 指向(不同类型)非const对象        (这样有啥用呢?)

    double i = 10.1;
    const int &refval = i; //指向一个临时量 10

    //编译器会将代码转换为
    double i = 10.1;
    int temp = i;
    const int &refval = temp;

右值引用&&见 拷贝控制->对象移动

auto 类型说明符

会根据初始值推算变量类型 auto一般会忽略顶层const 保留底层const auto varName = value; 根据 = 右边初始值推导类型 必须初始化

decltype 类型指示符

希望从表达式的类型推断要定义的变量类型 但是不想用该表达式的值初始化变量 decltype(exp) varName; 根据exp推导类型 不要求初始化

C++ 的 decltype 详细介绍_c++ decltype-CSDN博客

表达式

new和delete

动态创建和释放对象
new返回指向新创建对象的指针 不提供显式初始化的话,则用该类的默认构造函数初始化
delete释放指针所指向的地址空间 之后指针变成悬垂指针 要清空指针

    classname *cobj = new classname();
    ...
    delete cobj;
    cobj = nullptr;

classname *cobj = new classname(); //存放堆区 用户自己申请 释放
classname cobj; //存放栈区 系统自动管理内存

数组和指针

C++中基本用vector取代了数组 除非性能测试表面vector达不到速度要求才使用数组

数组的维数必须用大于等于1的常量表达式定义

type arr[const_expr] = {xx, xx, xx};

指针

指针保存的是另一个对象的地址 定义类型 type *name

(理解为 type型指针类型 (type *)型, 但是连续定义要分开 int *p1, *p2; )

解引用操作符* 取地址操作符& 区分: 给指针赋值 通过指针进行赋值
(我已经完全理解指针了x        然而还有指针的指针的指针....)

    int i = 1;
    int *ip = &i;    //取地址符号&
    cout << ip << endl;    //地址值    
    cout << *ip << endl;    //解引用(dereference)操作符* 所指对象

    int *ip2 = nullptr;    //理解为 ip2的类型是 int * 
    ip2 = ip1;    //给指针赋值    //相当于&(*ip);
    cout << ip2 << endl;    //地址值 ip2 == ip == &i == &(*ip)
    *ip2 = 10;    //通过指针给所指对象赋值 i = 10

nullptr空指针(最好用nullptr 不用NULL) 可以被转换成任意其他的指针类型
指针一定要初始化 避免野指针(不为空 但不知道指到哪里的指针)

指针可以用0值常量赋值 不能用0值的变量赋值

    int zero = 0; // NULL
    const int kZero = 0;
    int *pi = nullptr;
    pi = 0;    // ok
    pi = kZero;   // ok
    pi = zero;    // Error
void*指针

可以保存任何类型对象的地址 只与地址值相关 但不清楚此地址上的对象的类型
不允许使用void*指针操作它所指向的对象

兼容 接口 (回调)

指向const对象的指针 (底层const)

底层const表示指针所指对象是一个常量

(指针本身地址值可改 所指对象不可改/不可通过这类指针修改) (自以为指向const的指针)

const对象 只能用const对象的指针 指向 不允许通过const对象的指针修改对象 const对象的指针保存的地址值可以修改 即可以重新指向其他对象 也可以指向非const对象 但同样不能通过这个指针修改该对象

    const int i = 1;    //const对象
    const int *cptr = &i;    //指向const对象的指针
    错 int *p = &i;    //Error 非const对象指针 不能保存const对象的地址值
    错 *cptr = 2;      //Error 不能通过const对象指针修改所指对象
    int j = 10;        //非const对象
    cptr = &j;         //OK 可以同const对象指针指向非const对象
    错 &cptr = 20;     //Error 不能通过const对象指针修改所指对象

不能使用void* 指针保存 const对象的地址 必须用const void*

const指针 (顶层const)

顶层const表示指针本身是个常量

(指针自身地址值不可改)
不能对这个指针赋值 不能指向其他对象

    const int i = 1;       //const对象 顶层const
    int j = 10;            //非const对象
    const int *cptr = &i;  //指向const对象的指针 底层const
    int *const cp = &j;    //const指针 顶层const

    错 cp = nullptr;           //Error const指针自身的地址值不可修改
    const int *const cp2 = &i; //指向const对象的 const指针: 指针地址值不可修改 所指对象不可修改

函数

参数传递

如果形参具有非引用类型 则赋值实参的值 实现参数初始化
引用型参数则是直接调用所传递的实参本身

void Reset(int *ip)    //非引用形参 调用函数式 为实参副本
{
    *ip = 0;    //因为是指针 所以还是指向原本实参所指的对象 可以修改被指对象的值
    ip = 0;        //原实参指针的地址值没有改变
}


    int i = 20;
    int *p = &i;
    cout << *p << endl; //20
    Reset(p);    //修改的是*p 即i 没有改变p
    cout << *p << endl; //0
默认实参

void Func(int i , int j = 10) 调用Func时 Func(5) 相当于Func(5, 10)
设计带有默认实参的函数时 使最少使用默认实参的形参排在前面 最可能使用默认值的排在最后

在一个文件中只能为一个形参指定默认实参一次 可在函数声明 也可在函数定义指定

动态内存

自由空间 堆    用来存储动态分配的对象

智能指针

(第四版讲了一下多个指针指向同一地址的管理思路 第五版C++11加入了智能指针)

#include <memory>

智能指针是模板 shared_ptr<T> sp; 允许多个指针指向同一个对象
unique_ptr<T> up; 独占所指向的对象

p->mem    相当于(*p).mem
p.get() 返回p中保存的指针 慎用 如果智能指针释放了其对象 返回指针所指向的对象也消失了 swap(p, q)  交换p和q中的指针
p.swap(q)

shared_ptr 独有的操作: make_shared<T>(args) 最安全最高效的创建shared_ptr实例的方法 返回一个shared_ptr指针 指向一个动态分配的类型为T的对象 用args初始化此对象 shared_ptr<T>p(q)    p是shared_ptr q的拷贝 会增加q中的计数器 p = q     p和q都是shared_ptr 保存的指针必须能相互转换               此操作会递减p的引用计数 递增q的 若p的引用计数变为0则释放其管理的内存
p.use_count()    返回与p共享对象的智能指针数量 (可能很慢? 用于调试) p.unique()    p.use_count()为1则返回true

shared_ptr_shared ptr-CSDN博客

直接管理内存 new和delete

在堆分配的内存是无名的 因此new无法为其分配的对象命名 返回的是指向该对象的指针

string *ps1 = new string;    //默认初始化为空string
string *ps2 = new string();    //值初始化为空string
int *pi1 = new int;            //默认初始化: *pi1的值未定义 int没有默认构造
int *pi2 = new int();        //值初始化为0
vector<int> *pvec = new vector<int>{ 0,1,2 };    //可以使用列表初始化(花括号)

动态分配const对象 一个动态分配的const对象必须进行初始化 定义了默认构造函数的类类型可以隐式初始化

const int *pci = new const int(1024);    //分配并初始化一个const int
const string *pcs = new const string;    //分配并默认初始化一个const的空string

内存耗尽的情况下 new不能分配所要求的空间 会抛出一个bad_alloc的异常 可以通过nothrow阻止它抛出异常 int *p1 = new int;    //分配失败抛出bad_alloc int *p2 = new(nothrow) int; //分配失败 返回一个空指针

shared_ptr和new结合使用

不能将内置指针隐式转换为一个智能指针 必须使用直接初始化形式
还是推荐使用make_shared

shared_ptr<int> pi1 = new int(42);  //Error 不能隐式转换int* --> shared_ptr
shared_ptr<int> pi2(new int(42));   //OK 使用了直接初始化形式
shared_ptr<int> pi3 = make_shared<int>(42);    //最好用make_shared

定义和改变shared_ptr的其他方法 shared_ptr<T> p(q)    q为内置指针 p管理q所指向的对象 q必须指向new分配的内存 且能转换成T*类型    (如果这么定义p 是不是要让q指空比较好?) shared_ptr<T> p(u)    u为unique_ptr p从u那里接管对象的所有权 将u置为空 shared_ptr<T> p(q,d) 接管q所指的对象 p将使用可以调用对象d(lambda?)来代替delete shared_ptr<T> p(p2,d) p是p2的拷贝 区别在d p.reset()    如果p是唯一指向其对象的shared_ptr 则释放该对象                 直接打印p: 00000000 p.reset(q) 令p指向q

不要混用内置指针和智能指针!!

shared_ptr<int> pi1 = make_shared<int>(20);

cout << "pi1.use_count: " << pi1.use_count() << endl;    //1
auto pi2(pi1);    //auto方便
cout << "pi1.use_count: " << pi1.use_count() << endl;    //2
cout << "pi2.use_count: " << pi2.use_count() << endl;    //2
pi2.reset();
//pi2 = nullptr;    //感觉效果和reset差不多 计数器同样会-1
cout << "*pi1: " << *pi1 << endl;
cout << "pi1: " << pi1 << endl;    //地址值
cout << "pi2: " << pi2 << endl;    //00000000 为什么这么多个0:地址的格式

//cout << "pi2 = nullptr" << endl;
//delete pi1;
cout << "pi1.use_count: " << pi1.use_count() << endl;    //1
cout << "pi2.use_count: " << pi2.use_count() << endl;    //0

int *i = new int(10);
//int *ii = i;    //不影响shared_ptr的引用计数
shared_ptr<int> pi3(i);
//int* ii = pi3;    //不可转换
//delete i;    //如果先delete i pi3计数还是1 但指向对象的值未定义 -572662307
i = nullptr;
cout << "pi3.use_count: " << pi3.use_count() << endl;    //1
cout << "*pi3: " << *pi3 << endl;

unique_ptr

某个时刻只能有一个unique_ptr指向一个给定的对象 定义一个unique_ptr时需要将其绑定到一个new返回的指针上(没有类似make_shared的函数来返回一个unique_ptr)

unique_ptr<T> u1; // 空 可以指向T类型的对象 会使用delete来释放它的指针
unique_ptr<T, D> u2; // u2会使用一个类型为D的可调用对象来释放指针
unique_ptr<T, D> u3(d); // 使用类型为D的对象d代替delete
u = nullptr; // 释放u的对象 u置空
u.release(); // u放弃对指针的控制权 返回指针 u置空
u.reset();   // 释放u指向的对象
u.reset(p);  // 如果提供了内置指针p 令u指向这个对象 否则u置空

不支持普通的拷贝或赋值操作 但可以通过release或reset将指针所有权转移

unique_ptr<string> p2(p1.release()); // 将所有权从p1转移给p2 release将p1置空
unique_ptr<string> p3(new string("123"));
p2.reset(p3.release()); // reset释放p2原来指向的内存 将p3的所有权转移给

weak_ptr

C++智能指针weak_ptr详解_weakptr-CSDN博客

weak_ptr叫弱引用指针。是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。weak_ptr 设计的目的是为了协助shared_ptr而引入的一种智能指针,它可以解决shared_ptr循环引用的问题。weak_ptr只可以从一个shared_ptr或另一个 weak_ptr 对象来构造, 它的构造和析构不会引起引用记数的增加或减少。

指向一个由shared_ptr管理的对象 但不会改变shared_ptr的引用计数 一旦最后一个指向对象的shared_ptr被销毁 对象就会被释放 创建weak_ptr时 要用一个shared_ptr来初始化它

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);    // wp弱共享p p的引用计数未改变

weakptr<T> w;    空weak_ptr 可以指向类型为T的对象 weakptr<T> w(sp); 与shared_ptr sp指向相同对象的weak_ptr T必须能转换为sp指向的对象 w = p;         p可以是一个shared_ptr或者一个weak_ptr 赋值后w与p共享对象 w.reset(); 将w置空 w.use_count();    与w共享对象的shared_ptr的数量 不计weak_ptr的数量     w.expired();         若use_count为0 则返回true 否则false (expired 过期的 失效的) w.lock();          若expired为true 返回一个空shared_ptr 否则返回指向w的对象的shared_ptr

由于对象可能不存在 我们不能使用weak_ptr直接访问对象 而必须调用lock来检查weak_ptr指向的对象是否存在

#include <iostream>
#include <memory>
using namespace std;
class A {
public:
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {
    //循环引用导致ap和bp的引用计数都为2,
    //在离开作用域之后,ap和bp的引用计数只减为1,而没有减为0
    //导致两个指针都不会被析构,产生内存泄漏。
    //设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
    return 0;
}

//只需要将A或B的任意一个成员变量改为weak_ptr 或者都改为weak_ptr 就能解决引用循环的问题

类和数据抽象

不显著申明,则类会默认为我们提供如下几个函数:

(1)构造函数(A()) (2)析构函数(~A()) (3)拷贝构造函数(A(A&)) (4)拷贝赋值函数(A& operator=(A&)) (5)移动构造函数(A(A&&)) (6)移动赋值函数(A& operator=(A&&))

类的成员函数

类的成员函数可以访问该类的private成员

(提问: 能访问同类类型的private的成员对象吗 如二叉树的左右子树)

class Node
{
public:
    void Func(){Func(this);}
private:
    Node *l, *r;
    void Func(Node *node)
    {
        node->l->l->l;    //能否直接通过->
    }
}

在类外定义成员函数 要用作用于符号 type className::Func()

内联函数 inline

直接在头文件中定义
适用于优化小的且经常被调用的函数 以避免函数调用的开销
(内联说明对于编译器来说只是一个建议 编译器可以选择忽略)
头文件中加入或修改内联函数时 所有使用量该头文件的源文件都必须重新编译

this指针

每个成员函数都有一个隐含的形参this
在调用成员函数时 形参this初始化为调用函数的对象的地址

可以用于将自己作为成员函数的参数Func(this)

class A
{
public:
    int a = 10;
    void test()
    {
        int a = 100;
        this->a = a;    //局部变量a 赋值给成员变量a 两个a不冲突
    }
};

    A a;
    cout << a.a << endl;    //10
    a.test();
    cout << a.a << endl;    //100
const成员函数

void Func(xxx) const {...}
不能修改调用该函数的对象 就是说该函数只能读取成员变量

构造函数

是特殊的成员函数 与类同名 没有返回类型 可以有多个重载 public
如果没有定义 编译器会自动生成一个 没有形参的 默认构造函数

如果一个类没有定义自己的默认构造函数(无参数) 却有一个接受实参的构造函数(含参) 那么编译器将不合成默认构造函数 这个类的必须传递一个实参来显式定义
(总之 如果定义了其他的构造函数 则最好提供一个默认构造函数)

=default 显式要求编译器生成函数的一个默认版本 和写一个空的构造函数没什么区别 (据说是在移动构造和移动操作符上好处比较大)

class A{
public:
    A(int){};
    A() = default;
    //A(){}; // old way to get an empty constructors like default one.
    错 A(double) = default; //Error 含参构造 类型对于构造函数无效
};

=default修饰函数_= default-CSDN博客

构造函数初始化列表 可以初始化const对象或引用类型的对象 但不能对他们赋值    在开始执行构造函数的函数体之前要完成初始化 初始化const对象或引用类型的对象的唯一机会是在初始化列表中
除了这两个例外 对非类类型的数据成员进行赋值或使用初始化式在结果和性能上都是等同的 尽可能避免用成员来初始化其他成员 如果用 一定要注意顺序

class CTestClass
{
public:
    CTestClass() :rn_member_(n_test_member_),
        n_test_member_(0), str_test_member_("初始化列表") // 受定义的顺序
    //初始化类类型成员时 可以用该类的任意一个构造函数 
    //比如 str_test_member_(10,'a') 十个a
    {

    }
    ~CTestClass();

private:
    int n_test_member_;
    std::string str_test_member_;
    //int &rn_member_;    //可以初始化const对象或引用类型的对象 但不能对他们赋值 初始化的唯一机会是在初始化列表中
};

声明构造函数时 前面加上explicit 可以防止在需要隐式转换的上下文中使用构造函数 

重载函数

同名但形参表不同的函数
如果两个函数声明的返回类型和形参表完全匹配 则第二个算作重复声明}
如果形参表完全相同 但返回类型不同 则第二个声明错误 

析构函数

只有一个 没有形参 不能重载
在撤销类对象时 自动调用析构函数 回收或释放资源
合成析构函数(编译器自动生成的) 按对象创建时的逆序撤销每个非static成员
即使我们编写了自己的析构函数 合成析构函数仍然运行

友员friend

允许一个类将对其非公有成员的访问权授予指定的函数或类
通常将友员声明成组地放在类定义的开始或结尾

单例

static类成员

static数据成员独立于该类的任意对象而存在 于类关联 而不于类的对象关联
static成员函数没有this形参
static成员可以是私有的 全局对象不可以
static保留字只出现在类定义体内部的声明处

嵌套类

嵌套类在外围类的内部定义 但它是一个独立的类 基本上与外围不相干 嵌套类不可以访问外围类的成员 外围类可以通过对象访问嵌套类public成员 不能访问protected 和private成员

拷贝控制

拷贝构造函数

只有单个形参 该形参是对本类类型对象的引用(常用const修饰)

如果一个类想防止拷贝 必须显式声明其拷贝构造函数为private

含有指针成员的类 要注意拷贝时 拷贝的是指针的地址值 还是指针所指向的内容

浅拷贝: 就是对象的数据成员之间的简单赋值 默认拷贝构造为浅拷贝 深拷贝: 当拷贝对象中有对其他资源(如堆、文件、系统等)的引用或指针时,对象会另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值
(创建一个新的指针 指向的地址存放拷贝对象中原指针指向的内容)

重载操作符

ClassName& operator= (const ClassName&) { };

右值引用

(C++11)
右值引用&& 通过&&绑定到一个将要销毁的对象 右值通常分分为: 纯右值(字面常量)和将亡值(更侧重于自定义类型的函数的返回值,表达式的返回值) 1.只能绑定到右值 不能绑定左值 除非通过std::move将左值转换为右值 右值通常指的是临时对象、字面常量、表达式返回值等,它们没有名字且不可取地址。) 2.允许修改右值: 与const左值引用(可引用左值也可引用右值)不同,右值引用允许修改其所引用的右值 3.支持移动语义: 允许资源(如动态分配的内存、文件句柄等)从右值转移到左值,而无需进行复制,从而提高效率

    vector<int> vec(100);
    int&& r1 = f();    //int f();
    int& r2 = vec[0];
    int& r3 = r1;
    int&& r4 = f() * vec[0];
    const int& r5 = vec[0] * 42;    //const引用可以绑定到一个右值上

 std::move 将左值转换为右值
(使用std::move 不要直接使用move 避免名字冲突)
可以销毁一个move后的源对象 也可以赋值 但不能使用它的值  一文带你详细介绍c++中的std::move函数-CSDN博客

    int &&rr1 = 42;     //OK
    int &r1 = 42; // ERROR
    int &&rr2 = rr1;    //Error 具名右值引用rr1是左值
    int &&rr3 = std::move(rr1);    //OK 意味着除了对rr1赋值或销毁它外 将不再使用它

右值引用应用场景:

1.函数返回值:当函数需要返回一个局部对象时 可以使用右值引用来避免不必要的拷贝或移动,提高性能 2.移动构造函数 移动赋值运算符 3.完美转发

右值引用的作用: 没有右值引用的时候 函数参数如果是T &类型 会导致无法使用字面值或临时变量

void foo(std::string &s){ cout<<s<<endl;};
int main() {
    foo(string("123")); // X 无法接受右值
    foo("123");         // X 无法接受右值
    string s("123");
    foo(s);             // V 接受左值
    return 0;
}

可以将参数换成常左值引用 但是不能修改参数内容

void foo(const std::string &s){ cout<<s<<endl;}
int main() {
    foo(string("123")); // V 常左值引用接受右值
    foo("123");         // V 常左值引用接受右值
    string s("123");
    foo(s);             // V 常左值引用接受左值
    return 0;
}

这个时候右值引用就可以实现 既能使用字面值作为参数 也能通过引用修改参数

void foo(std::string &&s){ cout<<s<<endl;}
void foo(std::string  &s){ cout<<s<<endl;}
int main() {
    foo(string("123")); // V 常左值引用接受右值
    string s("123");
    foo(s);             // V 常左值引用接受左值
    return 0;
}
  • 在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。

现代C++之万能引用、完美转发、引用折叠-CSDN博客

万能引用

&&不一定表示右值引用

Widget&& var1 = someWidget;   // here, “&&” means rvalue reference

auto&& var2 = var1;          // here, “&&” does not mean rvalue reference
     // var2会被推测为 Widget &var2

template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference

// 万能引用 universal 
template<typename T>
void f(T&& param);           // here, “&&”does not mean rvalue reference

If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

如果一个变量或者参数被声明为T&&,其中T是被推导的类型,那这个变量或者参数就是一个universal reference

万能引用几乎只会出现在函数模板的参数和由auto声明的变量当中 只有在发生类型推导的时候 “&&” 才代表 universal reference

template<typename T>
void f(T&& param);

int a = 0;
f(a);    // 传入左值,那么上述的T&& a就是lvalue reference
f(10);    // 传入右值,那么上述的T&& 10就是rvalue reference

提问: vector中的push_back(T&& x) 和emplace_back(Args&&... args) 是万能引用吗     两个函数区别见 顺序容器-vector

template <class T>
void vector<T>::push_back(T&& x);
//push_back中参数类型T是确定的 不需要进行类型推导

template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);
// 而emplace_back接受可变参数 是一种万能引用

引用折叠 是万能引用的根本机制(看上边链接 真讲不清楚了) 简单来讲就是传入参数为左值时 折叠成&

完美转发

std::forward 完美转发 在传参的过程中保留 对象原生类型属性,即保持它的左值或者右值的属性

应用场景 :希望传入函数的右值能够保留右值走【移动构造】而不是【拷贝构造】

小结:

在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。

std::move()与std::forward()都仅仅做了类型转换而已。真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。

std::move()可以应用于左值(普通的变量int这些使用move与不使用move效果一样),但这么做要谨慎。因为一旦“移动”了左值,就表示当前的值不再需要了,如果后续使用了该值,产生的行为是未定义。

移动构造

移动构造函数的第一个参数是该类类型的右值引用 和拷贝函数一样,额外的参数都必须有默认实参
除了完成资源移动 移动构造还要确保移动后源对象处于一个 销毁无害 的状态

当移动构造未标记noexcept时该调用移动构造的时候可能调用的是拷贝构造(但不一定) 请为移动构造函数加上noexcept关键字_C/C++基础-CSDN专栏 比如这个帖子 我运行的结果和博主的不一样 vector自增长的时候就算没有noexcept也正常调用了移动构造

StrVec::StrVec(StrVec &&s) noexcept    //noexcept让函数不抛出异常
    //成员初始化器接管s中的资源
    : elements(s.elements), first_free(s.first_free), cap(s.cap)
{
    //令s进入这样的状态 对其运行析构函数时安全的
    s.elements = s.first_free = s.cap = nullptr;
}
struct Base1
{
    Base1() = default;
    Base1(const int &a) :n_val_(a) {}
    Base1(Base1 &&base1) noexcept
    {
        cout << "Base1 移动构造" << endl;
    }
    Base1& operator=(Base1 &&base1)
    {
        cout << "Base1 移动赋值" << endl;
    }
    void SetVal(int32_t x)
    {
        n_val_ = x;
    }

    void f()
    {
        cout << "Base1 f no para, n_val_ = " << n_val_ << endl;
    }

private:
    int32_t n_val_ = 0;
};


    Base1 *b1 = new Base1(11);
    b1->f();        // n_val_ = 11
    Base1 *b2(std::move(b11));
    b1->SetVal(110);
    b1->f();        // n_val_ = 110
    b2->f();        // n_val_ = 110
// 两个Base1的对象 看起来是指向了同一个

【C++11保姆级教程】移动构造函数(move constructor)和移动赋值操作符(move assignment operator)-CSDN博客

C++ 移动构造函数详解-CSDN博客


顺序容器

vector        支持快速随机访问 插入删除开销大

list        支持快速插入删除 查找需要完全遍历

deque        双端队列

容器的多数操作通用 以vector为例

迭代器

vector vec;
vector ::iterator iter;        //类似于指针

vec.begin(); //如果vec不为空 则begin返回指向第一个元素的迭代器 相当于vec[0]
vec.end();    //指向末端元素的下一个(不指向任何元素 超出末端)
如果vec为空 则begin()==end()
iter++; //向后移动迭代器 指向下一个元素
iter--;   //向前移动迭代器 指向上一个元素
iter == vec.begin() 时不能--
iter == vec.end()    时不能++
iter1 – iter2 两个迭代器之间间距 类型difference_type 可以是负数 为unsigned型
(第四版P269 iter1 +=/-= iter2 这种操作应该是错的)

逆序迭代器
vector<int>::reverse_iterator iter = vec.rbegin(); rend()
const迭代器
vector<int>::const_iterator iter = vec.cbegin(); cend()

迭代器范围 左闭右开 [ib, ie)

vector<T>

各个顺序容器有类似的初始化写法
    vector v1;          //空
    vector v2(v1);    //copy
    vector v3(n,i);    //n个值为i的元素
    vector v4(n);      //n个初始化值的元素
    vector v5 = { e1, e2, ... };   //指定元素 也可以不用 =赋值运算符
    vector v6(iter1, iter2);    //创建两个迭代器范围内的副本 注意迭代器先后顺序

容器遍历

下标遍历        for (vector::size_type index = 0; index < vec.size(); index++) 迭代器遍历    for (vector::iterator iter = vec.begin(); iter != vec.end(); iter++)
范围for遍历   for (auto x : vec)

vector自增长原理

vec.size()        大小 当前元素个数
vec.capacity() 容量 分配更多空间之前可存储的元素数量
        当预留容量耗尽时(size == capacity) 再添加新元素时 vector会自动分配空间
        根据不同的库策略不同 书上是x2 (我的环境里跑出来是x1.5)
vec.reserve(n) 预留至少n个元素的空间 如果n小于当前size或capacity 则无事发生

顺序容器操作

任何insert push 或erase操作 都可能导致迭代器失效
deque首尾插入元素不会导致其他迭代器失效 其他位置的增删还是会导致失效
总之 尽量在各种操作之后更新迭代器

添加元素
c.push_back(t)        返void
c.push_front(t)        返void 只用于list 和deque
c.insert(iter, t)          iter前插入t 返回新元素t的迭代器
c.insert(iter, n, t)      iter前插入n个t 返回void
c.insert(iter, ib, ie)   iter前插入[ib, ie) 标记范围内的元素 返回void

push_back和emplace_back的区别

push_back需要先创建一个对象,然后将其拷贝或移动到容器中,而emplace_back则是直接在容器内存中构造对象,无需额外的拷贝或移动操作

push_back的工作原理

  • push_back函数是在容器末尾添加一个已经构造好的对象的副本(或移动副本,如果使用了右值引用)。
  • 当向容器添加元素时,push_back通常涉及到拷贝或移动构造函数,因为它需要一个完整的对象作为参数。
  • 在C++11之后,如果传入一个临时对象,push_back可以利用移动语义来减少拷贝开销。

emplace_back的工作原理

  • emplace_back函数直接在容器的尾部构造元素,它可以接受任意数量和类型的参数,这些参数正是容器中元素类型的构造函数所需要的。
  • 使用emplace_back可以避免临时对象的创建和可能的拷贝或移动操作,因为它是在容器内存空间中直接构建对象的。
  • 对于含有非复制或移动构造的对象来说,emplace_back尤其有用,因为它允许在容器中直接构建复杂对象。

性能差异和选择建议

  • 对于简单数据类型(如int、float、指针等),push_back和emplace_back的效率差别并不明显。
  • 如果您已经有一个对象实例并且想要将其添加到容器中,使用push_back是合适的。
  • 如果您想要构造一个新对象并直接将其放到容器中,使用emplace_back可以避免额外的拷贝或移动操作,从而更为高效。

综上所述,选择push_back还是emplace_back取决于具体的使用场景:如果您需要添加已经存在的对象实例到容器中,push_back是一个不错的选择;而如果您需要在容器中直接构造新对象,并且希望避免额外的拷贝或移动操作,那么emplace_back是更优的选择。‌

一篇文章搞懂 push_back 和 emplace_back 的区别-CSDN博客

大小
c.empty()       是否为空 返回bool
c.size()          大小 元素个数 返回c::size_type类型 (不定 是signed型) 最好不要赋值给int防止溢出
c.max_size() 可容纳的最多元素个数 返回size_type
c.resize(n)     重置元素个数为n 删除多的/添加采用值初始化的新元素
c.resize(n, t)  同上 新增值为t的元素

删除
c.erase(iter)       删除iter指向的元素 返回被删元素后一个元素的迭代器
c.erase(ib, ie)     删除[ib, ie)  返回同上 即返回ie
c.clear()              清空 返回void
c.pop_back()      返void
c.pop_front()     返void 只用于list 和deque

赋值
c1 = c2;         相当于 先清空c1.clear(); 在插入c1.insert(c1.begin(), c2.begin(), c2.end());
c1.swap(c2)  互换 迭代器不会失效 迭代器指向交换后的位置 iter1-->c1[0] <===> iter1-->c2[0]
                      执行速度通常比将c2的元素复制到c1的操作快(节省删除/移动元素的成本)
c.assign(ib, ie)  重置c的元素 先清空在插入 ib,ie不能是c中的迭代器 因为重置之后会失效
c.assign(n, t)     重置为n个t clear(); insert(begin(), _Count, _Val);

c.assign会清空目的vector c后再将源vector的值全部插入到目的vector中

    不会缩小capacity 正常自增长

效率:

通常来说vector之间复制操作效率 swap > copy > assign > 直接赋值 > push_back赋值 这里比较好的是swap和copy,但需要注意的是,swap是内存交换(交换两个vector的头指针),这里经过swap后,src的内容会与dst发生交换。其次copy也不错,不过需要注意的是,swap需要提前分配好足够的内存,比如在声明是分配或者用resize分配(不能用reserve,二者区别可以去网上查),否则会导致程序崩溃

两个vector之间复制数据的效率比较试验_vector assign效率-CSDN博客

顺序容器适配器

stack 栈 LIFO后进先出

queue 队列 FIFO先进先出
priority_queue 优先级队列

可以用顺序容器初始化一个新的适配器 默认stack和queue都是基于deque实现的 也可以通过第二个类型实参指定顺序容器来覆盖基础容器类型
stack stk(deq)
stack<int, vector> stk(vec);
queue 要求push_front运算 只能建立在list上
priority_queue 可以vector或deque

操作
stack.empty()
s.size()
s.pop()        弹出栈顶 返回void 有些语言会返回栈顶元素
s.top()         返回栈顶元素
s.push(t)     压栈
queue.pop()删队首
q.front()
q.back()
priority_queue.top()  返回最高优先级元素


关联容器

标准库提供8个关联容器 主要在三个维度上有区别:
(1)要么是map 要么是set (2)是否允许关键字重复 (3)是否有序保存元素

map<key, value> 关联数组 键值对(关键字- 值)

set<key>        关键字即值 只保存关键字

multimap        关键字可重复的map
multiset          关键字可重复的set
unordered_map          用哈希函数组织的map
unordered_set            用哈希函数组织的set
unordered_multimap  用哈希函数组织的map 关键字可重复
unordered_multiset    用哈希函数组织的set 关键字可重复

map<key, value>

    map<string, size_t> word_count;    //单词计数
    string word;
    while (cin >> word)
    {
        //下标操作 以string类型的word作为下标
        //如果word还未在map中 则会创建一个新元素 值为0
        word_count[word]++;
    }

    for (const auto &w : word_count)
    {
        //从map中提取的元素 得到的是pair类型对象
        //map所使用的的pair用first成员保存关键字(key) second成员保存值(value)
        cout << w.first << " occurs " << w.second << " times" << endl;
    }

 范围for语句遍历map 也可以用迭代器遍历
从map中提取出的元素是pair对象p p.first保存关键字 p.second保存对应的值

set<key>

set multiset可以用vector迭代器构造
    vector vec{ 0,0,1,1,2,2,3,3,4,4 };   
    set iset(vec.begin(), vec.end());            //size == 5  包含vec中不重复的元素
    multiset miset(vec.begin(), vec.end());  //size == 10

set.find(key) 返回迭代器 如果key在set中 迭代器指向该关键字 否则返回end()

multiset.find(key) 返回找到的第一个元素的迭代器 multiset.equal_range(key) 返回pair迭代器范围

有序容器自定义顺序

bool CompareEle(const typex &lx, const typex &rx)
{
    return lx < rx;
}

    multiset<typex, decltype(CompareEle)*> miset(CompareEle);

关联容器操作

key_type        容器关键字类型
mapped_type 关键字关联的类型(即值类型 ) 只用于map 
value_type     相当于元素类型 对于set 与key_type相同
                       对于map 为pair<const key_type, mapped_type>
(这个命名就很怪 value_type很容易想到键值对的值吧 元素类型叫ele_type多好)

set和map 的关键字都是const的 就是说set的元素是const的 map的元素(pair)的first是const的

添加元素
set.insert(ib, ie)
set.insert({ x, x, x, ...});  插入已存在的元素对set没有影响

    map<string, size_t> word_count;    //单词计数
    string word;
    //向word_count插入单个word的四种方法
    word_count.insert({ word, 1 });    //{"word", 1}
    word_count.insert(make_pair(word, 2));
    word_count.insert(pair<string, size_t>(word, 1));
    word_count.insert(map<string, size_t>::value_type(word, 1));
    //迭代器范围
    word_count.insert(word_count.cbegin(), word_count.cend());

添加单一元素的insert和emplace 返回值是pair<value_type, bool>
返回值pair的first是插入元素的迭代器 second布尔值表示插入是否成功 如果关键字已存在则false
对于map来说 返回pair<pair<key, value>, bool>
    auto p = word_count.insert({ word, 1 });
    cout << "key: " << p.first->first << " value: " << p.first->second << endl;
(p.first->first这里的-> 给编译器整不会了 不能自动把.转换 提示的列表里没有firs和second )

删除
c.erase(k)           从c中删除关键字为k的元素 返回size_type 指出删除的元素数量
c.erase(iter)        删迭代器指向的元素 返回后一个元素的迭代器
c.erase(ib, ie)      删除[ib, ie)  返回同上 即返回ie

访问
map下标
map<string, size_t> word_count;
word_count["OW"] = 2016;
会执行如下操作:
        先在word_count中搜索关键字为OW的元素 未找到
        将新的键值对插入word_count 关键字是const string 保存OW 值初始化为0
        提取出新插入的元素 赋值为2016
word_count.at("OW") 访问关键字为OW的元素 如果不存在OW 则抛出异常

如果只是想知道一个关键字是否存在map中 而不想改变map 则不能用下标 要用find
存在则返回该迭代器 不存在返回end()

c.find(k)
c.count(k) 返回关键字等于k的元素的数量 对于不允许重复的容器 返回值只有0和1
c.lower_bound(k)  返回指向第一个关键字不小于k的元素的迭代器
c.upper_bound(k)  返回指向第一个关键字大于k的元素的迭代器
c.equal_range(k)   返回一个迭代器pair<ib, ie>表示关键字等于k的元素范围 不存在则两个end

允许重复关键字的容器multiset或者multimap 如果有多个元素有相同的关键字 这些元素会相邻存储


面向对象编程 OOP

继承

派生类(derived class) 能够继承基类(base class) 定义的成员

派生类到基类的类型转换:因为派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当成基类对象来使用 也能将基类的指针或引用绑定到派生类对象中的基类部分上 但不能将基类转换成派生类

DerivedClass derived;
BaseClass *basep = &derived;    //OK 动态类型是DerivedClass
错 DerivedClass *derivedp = basep;    //Error 不能将基类转换为派生类
// 可以强转 但不建议
dynamic_cast
// 

类对象之间不存在类型自动转换 基类可以用派生类作为自己拷贝构造的参数进行初始化或赋值 但只负责拷贝其中的基类部分 派生类部分会被忽略

容器与继承

    vector<CBase> vec_class;
    vec_class.push_back(CBase());
    vec_class.push_back(CPubDerv());    //OK 但只能把派生类中基类的部分拷贝给vec

如果希望在容器中存放具有继承关系的对象时 可以存放基类的指针

如果基类定义了一个static静态成员 则整个继承体系中只存在该成员的唯一定义

派生类的成员将隐藏同名的基类成员

防止继承 不希望一种类被继承 在类名后跟一个关键字final
class ClassName final { ... };

可以通过using 继承基类中的构造函数 using Base::Base; 遇到这条代码的时候,会把基类的每个构造函数,都生成一个与之对应的派生类构造函数。 Base(构造函数形参列表...) : D1(照抄的构造函数形参列表){}; 函数体为空 Base(int i, int j, int k) : D1(i,j,k){}

在基类带有默认参数的构造函数的时候 对于继承构造函数来说 参数的默认值不会被继承 但会导致基类产生较多个构造函数的版本

using也可以防止基类中的同名函数被隐藏 C++11之继承构造函数(using 声明)_using 构造函数-CSDN博客

struct Base1
{
    Base1() = default;
    Base1(int a);

    void f()
    {
        cout << "Base1 f no para " << endl;
    }

    void f(int a)
    {
        cout << "Base1 f : " << a << endl;
    }
};

struct D1: public Base1
{
    using Base1::f;    //不会隐藏Base1中的void f(int a)
    void f()
    {
        cout << "D1 f" << endl;
    }
};

int main()
{
    D1 d1;
    d1.f();    //D1中的f   
    d1.f(12);  //参数不同 调用Base1中含参的f  
               //如果没有using Base1::f; D1中只要有同名函数 则Base1的f被隐藏
    d1.Base1::f();    // 不用using的话 也可以通过作用域 调用Base1中被隐藏的无参f()
    d1.Base1::f(34);  // 如果没有using Base1::f; 可以通过作用域调用基类成员函数
}

多重继承

派生类的构造函数初始化列表将实参分别传递给每个直接基类 其中基类的构造顺序与派生列表中基类的出现顺序保持一致 而于派生类构造函数初始化列表中基类的顺序无关

多继承中 using继承构造函数可能导致冲突 由于基类中有相同的构造函数导致的子类中有重复定义类型相同的继承构造函数

struct Base1
{
    Base1() = default;
    Base1(int &a);
};

struct Base2
{
    Base2() = default;
    Base2(int &b);//含参构造不能默认 = default;    //形参会影响using?? 确实
};

struct D1: public Base1, public Base2
{
    using Base1::Base1;    //继承构造函数
    using Base2::Base2;    //Error D1::D1(int &a)已从%t继承
    //有相同参数列表的构造函数 所以冲突

    //D1(int &a) {};    //可以通过显式声明冲突的构造
};

14.15 继承的构造函数、多重继承、类型转换与虚继承_using继承父类构造函数-CSDN博客

虚函数

定义为virtual的函数(虚函数)是基类期待派生类重新定义的
基类希望派生类继承的函数不能定义为虚函数
基类通常都应该定义一个虚析构函数
一旦某个函数被声明为虚函数 则在所有派生类中都是虚函数
如果虚函数使用默认实参 基类和派生类中定义的默认实参最好一致

当使用指针或引用调用虚函数时 该调用将被动态绑定 根据所绑定的对象类型不同 该调用可能执行基类的版本 也可能执行某个派生类的版本

派生类可以显式地注明它使用某个成员函数覆盖了它继承的虚函数 加上override
一个派生类的函数如果覆盖某个继承而来的虚函数 它的形参类型必须与该基类函数完全一致

回避虚函数的机制 希望对虚函数调用不要进行动态绑定 强迫执行某个特定版本时 可以使用作用域

可以将某个函数指定为final 将不允许之后尝试覆盖该函数的操作

纯虚函数

type Func() = 0; 纯虚函数无需定义 在函数体位置写上=0 (只能出现在类内部的虚函数声明语句处)

含有纯虚函数的类 是抽象基类 负责定义接口 不能(直接)创建一个抽象基类的对象

访问控制

protected用来声明它希望与派生类分享但不想被其他公共访问使用的成员
派生类的成员或友员只能通过派生类对象来访问基类的受保护成员

private继承不影响派生类的访问权限

D类继承自B类
public继承 D继承自B类的成员访问权限不变
protected继承 B的public成员变成protected
private继承 B的成员全部变成private
c++中protected和private区别_c++ protected访问权限范围-CSDN博客

 struct和class的区别 只有默认访问说明符不同 struct默认public class默认private

using可以保持继承的成员访问级别 派生类只能为那些它可以访问的名字提供using声明

例如父类A中public 的成员a,通过private继承时,父类的a变为私有的,可以通过using A::a;语句保持a是public的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值