C++进阶知识点
C++进阶知识点

C++进阶知识点

知识点后的“p+数字”代表该知识点在《C++ Primer(第5版)》中的页码。

字面值常量 p35

初始化与赋值的区别 p39

void*指针 p50
仅关心指向的地址而不关心指向的内容的类型。

顶层const与底层const p57
顶层const指指针本身是个常量,即所指地址固定;底层const指指针所指的是个常量,而指针本身可以指向不同地址。

常量表达式 p58

constexpr类型(C++11) p59
类型修饰符,在声明变量的类型前加上,则可通过编译器在编译阶段时判断用于初始化变量的表达式是否是常量表达式(不是则会报错)。

using关键字别名声明(C++11) p60
using pint = *int 作用和typedef相同。

auto类型说明符(C++11) p61
通过初始化变量的表达式自动赋予变量类型。

decltype类型指示符(C++11)p62
decltype(表达式) 变量名;通过表达式的类型决定变量的类型。

string与字符串字面值(const char *) p81

范围for(C++11) p82 p168

vector类型 p86

迭代器运算 p99

指针迭代器 p105

begin和end标准库函数(C++11) p106
区别与迭代器的begin和end函数,此处的是标准库中的函数,而非迭代器对象的成员函数。
用法为begin(数组),返回的是一个指针(同时也是迭代器)。

C风格str函数 p111

位运算符 p136

新式的强制类型转换(static_cast、dynamic_cast、const_cast、reinterpret_cast) p144
用法:cast-name<type>(expression);static_cast用于不包含底层const的类型转换;const_cast用于“去const化”;reinterpret_cast用于在底层上强行改变类型的解释(如果使用不注意会引发一些错误)。

switch内部的变量定义 p163
switch中可以声明但是不可以初始化变量。

局部静态对象 p185
局部变量加上static后,声明周期可以从被初始化开始一直持续到程序结束。

处理命令行选项 p196

initializer_list形参(C++11) p197
举例:void func(initializer_list<string> li)
该函数可以接受一个变长的参数表,每个参数类型为string
调用:func({“a”,”b”});
或:func({“a”,”b”,”c”});
常用在构造函数,实现初始化列表

尾置返回类型(C++11) p206
举例:auto func(int i) -> int(*) [10];
该函数声明返回一个含有十个整数的数组
与int (*func(int i))[10]效果相同,但更加明晰。

内联函数与constexpr函数(C++11) p213
inline关键字加载函数声明前,可以让编译器在函数调用位置内联地展开函数,从而消除函数运行的额外开销,适用于比较简单的函数。constexpr函数则是只有一句return语句,返回的是一个常量表达式,可以用于其他常量表达式的场合。
建议将这两种函数放在头文件中使用。

assert预处理宏与NDEBUG预处理变量 p215

__func__、__FILE__、__LINE__、__TIME__、__DATE__ p216

函数指针 p221
return_type (*val_name)(parameter_type)

构造函数=default p237

友元 p241
在一个类的声明中,若想要其他类外部的函数访问类内部的private成员,需要在类的内部用friend关键字添加允许访问的外部函数的声明。
如:

class d{
    friend d add(const d&,const d&);
private:
    int num = 1;
public:
    void print(){
        cout<<num<<endl;
    }
};
d add(const d &a,const d &b){
    d sum = a;
    sum.num += b.num;
    return sum;
}

add函数可以访问到d类中的private成员num。

类之间的友元关系 p250
friend class class_name
或只将另一个类的成员函数作为友元:friend return_type class_name::func_name(parameter)

构造函数初始值列表 p258
举例:class_name(int ii): i(ii), ci(ii), r(ii){}
该构造函数接受一个参数ii,然后将类的成员变量i、ci、r用ii初始化。

委托构造函数(C++11) p261
简单来说,构造函数可以将自己的工作委托给其他构造函数。
举例:class_name(string s, unsigned cnt, double price):bookNo(s), units_sold(cnt), revenue(cnt*price){ ... }
class_name():class_name("",0,0){ ... }
上例当中,默认构造函数(空参数列表)将初始化的工作交给了接受3个参数的构造函数,并提供一个空字符串,0和0,三个参数。

隐式的类类型转换 P263
如果构造函数只接受一个实参,那么他实际上定义了一个隐式的转换机制。该类构造函数称为转换构造函数。
要抑制这种隐式转换,要在构造函数前加explicit关键字。

聚合类 p266
一个类满足以下条件则称为聚合的:
·所有成员都为public
·没有定义任何构造函数
·没有类内初始值
·没有基类,也没有虚函数
聚合类可以用该方式初始化数据成员:

struct Data{
    int ival;
    string s;
}
Data val1 = { 0, "Anna" };

类的静态成员 p269
可以简单地理解为,静态成员在所有对象中共享,且只有一个副本。
声明时用static关键字。
使用时用作用域运算符访问:val = class_name::static_member
用点运算符也可以:val = obj.static_member
前提是static_member是public的。

lambda表达式(C++11) p346
可以理解为一个匿名的内联函数。
形式:[capture list](parameter list) -> return type {function body}
capture list(捕获列表)是lambda所在函数中定义的局部变量列表(通常为空)。
lambda必须用尾置返回。
参数列表和返回类型可忽略,但捕获列表和函数体必须有:auto f = [] {return 42;};
lambda不能拥有默认参数。

关于lambda的捕获列表 p350
[] 不捕获任何变量。
[&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
[=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
[=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
[bar] 按值捕获 bar 变量,同时不捕获其他变量。
[this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。

class A
{
    public:
    int i_ = 0;
    void func(int x, int y)
    {
        auto x1 = []{ return i_; };                    // error,没有捕获外部变量
        auto x2 = [=]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x4 = [this]{ return i_; };                // OK,捕获this指针
        auto x5 = [this]{ return i_ + x + y; };        // error,没有捕获x、y
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕获this指针、x、y
        auto x7 = [this]{ return i_++; };              // OK,捕获this指针,并修改成员的值
    }
};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [&]{ return a++; };            // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; };              // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; };          // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); };  // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); };  // OK,捕获所有外部变量和b的引用,并对b做自加运算

//延迟调用
int a = 0;
auto f = [=]{ return a; };      // 按值捕获外部变量
a += 1;                         // a被修改了
std::cout << f() << std::endl;  // 输出的仍然是0

//mutable关键字
int a = 0;
auto f1 = [=]{ return a++; };             // error,修改按值捕获的外部变量
auto f2 = [=]() mutable { return a++; };  // OK,mutable

bind函数(C++11) p354
形式:auto newCallable = bind(callable, arg_list);
形如_1、_2、_3等的为占位符,在arg_list中代表接受的参数。
例如:auto check6 = bind(check_size, _1, 6)
调用时:string s = "Hello";
bool b1 = check6(s); //会调用check_size(s,6)

关联容器(map、set) p374
map的元素是键值对,set是集合。
分为不可重复和可重复:如map关键字不可重复,multimap关键字可重复。
再分有序和无序:如multiset有序,unordered_multiset无序。
无序无法用迭代器遍历,但是查找很快(基于哈希)。
map.first访问键,map.second访问值。

pair类型 p379
可以理解为键值对,map就是由多个pair组成的。
C++11标准中,可以直接用花括弧构造一个pair对象:

pair<string, int> process(vector<string> &v){
    return {v.back(), v.back.size()}
}

智能指针(C++11) p400
智能指针的行为类似常规指针,区别在于智能指针可以自动释放所指的对象。
标准库提供两种智能指针:shared_ptrunique_ptr,另外还定义了一个名为weak_ptr的伴随类。
三种类型都定义在memory头文件中。
声明举例:shared_ptr<string> p1;
上面声明了一个可以指向string类型的共享指针
智能指针可以直接用在if语句中检查是否为空:if(p1){}
智能指针的get方法可以获取对应的指针:string *ps = p1.get(),但是要小心使用。
用swap方法可以交换指针:swap(p, q)或p.swap(q)

shared_ptr(C++11) p400
shared_ptr维护一个“引用计数”,当引用计数为0时,释放其管理的内存。
p.use_count()返回与p共享对象的智能指针数量,主要用于调试。
p.unique():若与p共享对象的智能指针数量为1(即只有p自己),则返回true,反之返回false。

make_shared函数(C++11) p401
make_shared<T> (args)返回一个shared_prt,指向一个动态分配的类型为T的对象。使用args初始化此对象。
创建shared_ptr最安全的方法就是调用make_shared函数。
一种较为简单的创建shared_ptr的方式(使用auto):auto p = make_shared<string>("Hello");

定位new p408
定位new表达式可以向new传递额外的参数,如:
int *p1 = new int; //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针

上面我们就传递了nothrow对象给new,让new不抛出异常。

空悬指针 p411
仅delete关键字释放的指针会变成空悬指针。
区别与空指针,空悬指针指向一个无效的地址,而空指针不指向任何地址。

使用new初始化智能指针 p412
由于智能指针的构造函数时explicit的,所以在构造时需要使用直接初始化形式:
shared_ptr<int> p1 = new int(1024); //错误
shared_ptr<int> p2(new int(1024)); //正确

unique_ptr(C++11) p417
不同于shared_ptr,某个时刻只能有一个unique_ptr指向给定对象。
所以unique_ptr不支持普通的拷贝或者复制操作。
release函数用于放弃对指针的控制权,并返回这个指针。
reset用于释放所指对象。
转移所有权:p2.reset(p3.release());

weak_ptr(C++11) p420
weak_ptr是一种不控制所指对象生存期的智能指针,他指向一个由shared_ptr管理的对象。
但是weak_ptr不会改变shared_ptr的引用计数。
使用shared_ptr来初始化weak_ptr:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);

不能直接使用weak_ptr直接访问对象,而必须调用lock函数。如:
if(shared_ptr<int> np = wp.lock()){//如果np不为空则条件成立
//在if中,np与p共享对象
}

weak_ptr的主要用途是不影响所指对象的生命周期,但是可以组织用户访问一个不再存在的对象。

allocator类* p427
使用allocator类,可以构造一个用于分配某种类型所需的动态内存的分配器,如:
allocator<string> alloc;
auto const p = alloc.allocate(n);

上例中,分配了可以存放n个string的内存空间,给了p指针。
allocate仅仅是分配而已,并不会构造对象。
要构造对象,需要调用construct函数:
auto q= p;
alloc.construct(q++);//*q为空字符串
alloc.construct(q++,10,'c');//*q为cccccccccc
alloc.construct(q++,"hi");//*q为hi

运算符重载 p490
运算符的重载声明为一个函数,例子:
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs){ ... }
上例将运算符“+”重载用于对两个Sales_data进行计算并返回一个Sales_data的操作。
调用时可以做到:Sales_data result = a + b;
a和b都为Sales_data的对象。
当运算符重载函数是一个类的成员函数时,则左侧的运算对象隐式地绑定在this指针上,右侧则为函数的参数。
如:

class complex{
public:
    complex operator+(const complex &A) const;
private:
    double m_real;  //实部
    double m_imag;  //虚部
};

complex complex::operator+(const complex &A) const{
    complex B;
    B.m_real = this->m_real + A.m_real;
    B.m_imag = this->m_imag + A.m_imag;
    return B;
}

上例中重载了+运算符,并且只有一个参数,左侧的运算对象隐式地绑定在了this指针上。

function类型(C++11) p512
例子:function<int(int,int)> f;
上例声明了一个用于储存可调用对象的function对象f,其返回一个int,并接受两个int。
通过运算符的重载,f可以拥有和可调用对象相同的行为。
有了function类型,我们可以通过map来实现一个函数表:
map<string, function<int(int,int)>> binops;

final关键字(C++11) p533
有时候我们不希望有些类被继承,我们则可以使用final关键字声明该类为不可继承。
class NoDerived final { ... }
派生类也可以声明为final:
class Last final : Base { ... }
final关键字还可以用于不想被覆写的函数:
void f1(int) const final;

虚函数 p536
虚函数的意义在于多态性,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
区别于直接覆写,直接覆写普通函数基类指针只能访问到基类中的函数版本,即使基类指针指向的是派生类对象。
虚函数在基类中声明,声明方式是在函数声明前加virtual关键字。
如:virtual void print() { ... }

override关键字(C++11) p538
override的主要用途是标记覆写的函数,明确该函数是要进行覆写。从而在编译阶段可以发现覆写错误。
使用方式是在函数声明的参数列表后加override关键字:
void func(int a) override { ... };

纯虚函数 p540
有时候基类不一定能给出某个函数的具体定义,需要基类自己去实现,这时候要将该函数声明为纯虚函数。
区别于虚函数,纯虚函数不要求定义(但可以定义),声明方式是在参数后加“=0”:
double net_price(size_t) const = 0;

抽象基类 p541
含有纯虚函数的类是抽象基类,它不能直接被实例化,只能被继承。

派生访问说明符 p543
派生访问说明符加在派生类声明中基类名称的前面:
class chlid : private parent{ … }
作用是派生类用户(包括派生类的派生类)对于基类的访问权限。
参考以下例子:

class Base {
public:
    void pub_mem();
};
class Pub_Derv : public Base {};
class Priv_Derv : private Base {};

Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem();//正确,pub_mem对于派生类Pub_Derv是public的
d2.pub_mem();//错误,pub_mem对于派生类Priv_Derv是private的

在类内部使用using语句 p545
在类内部使用using语句可以改变派生类中基类成员的可访问性:

class Base {
public:
    std::size_t size() const { return n; }
protected:
    std::size_t n;
};
class Derived : private Base {
public:
    using Base::size;
protected:
    using Base::n;
};

Derived私有继承Base,size和n在默认情况下是Drived的私有成员,而用了using关键字之后,size将变成public成员,n将变成protected成员。

虚析构函数 p552
虚析构函数的意义同样在于多态:一个基类指针指向的可能是继承体系中的任何一个类的对象,在对基类指针使用delete关键字时,需要执行对应类版本的析构函数。要达到这个效果,就需要使用虚析构函数。
虚析构函数的形式:virtual ~class_name() { ... };

函数模板 p578
当我们需要相同的函数能够处理不同类型的数据(只是参数不同),不需要写多个重载函数,而可以直接用函数模板。
函数模板使用关键字template声明,例子:
template <typename T> int compare(const T &v1, const T &v2){ ... }
调用时会自动根据提供的参数类型判断T。

类模板 p583
类模板的声明方式:

template <typename T> class TemplateClass{
public:
    T func(const T &t);
private:
    T data;
}

上例中声明了一个类模板,其中有两个成员,一个成员函数和一个成员变量,成员函数和成员变量中可以使用T作为类型。

实例化类模板 p584
不同于函数模板,类模板不会自动推断类型,在使用时需要显示模板实参,调用方式例子:
TemplateClass<int> obj;

可变参数模板(C++) p618
可变参数模板就是一个接受可变数目的模板函数或模板类,可变数目的参数称为参数包。
使用形式如:typename... param_pac_name表示一个参数包。
可变参数模板函数声明例子:template <typename T, typename... Args> void foo(const T &t, const Args& ... rest);
在函数的定义中,如果我们需要知道包中有多少元素时,可以使用sizeof...运算符。
例如:sizeof...(Args)sizeof...(rest)
可变参数模板函数的编写通常是利用递归:

//用于终止递归和打印最后一个元素
//此函数必须在可变参数版本的print定义之前声明
template <typename T> ostream &print(ostream &os, const T &t){
    return os<<t;
}
template <typename T, typename... Args> ostream &print(ostream &os, const T &t, const Args&... rest){
    os<<t<<",";
    return print(os, rest...);
}
int main() {
    print(cout,"abc",2,'c',4.44);
}

上例中的print函数可以打印各种不定个数的不同类型的参数。

tuple类型(C++11) p636
tuple类型(元组类型)与pair类似,区别是pair只有两个成员,而tuple可以有任意多个成员,且成员之间的类型可以互不相同。
tuple用于一些不想定义一个新的数据结构但是想把一些数据组合成单一对象的场合。
我们可以将tuple看作一个“快速而随意”的数据结构。
tuple的声明方法:tuple<T1, T2, ..., Tn> t;
或使用make_tuble函数:make_tuple(v1, v2, ..., vn); tuple的成员类型会通过参数类型了判断。
获取一个元组t中的第i个成员的引用时:get<i>(t);

正则表达式*(C++11) p645

随机数库(C++11) p659
区别于旧的rand函数,rand函数既无法生成浮点数,也无法生成服从一定随机分布的数。
新标准中定义了一个头文件random,其中包含一些类,如随机数引擎类随机数分布类。
引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数。
调用随机数引擎类生成原始随机数:

default_random_engine e;//生成无符号随机数
for(size_t i = 0; i < 10 ; i++){
    cout<<e()<<" ";//e()调用对象来生成下一个随机数
}

不过对于大多数场合来说,随机数引擎输出的数是不能直接使用的。
为了得到一个在指定范围内的数,我们使用一个分布类型的对象:

int main() {
    uniform_int_distribution<unsigned> u(0, 9);//生成0到9之间均匀分布的随机数
    default_random_engine e;//生成无符号随机数
    for(size_t i = 0; i < 10 ; i++){
        cout<<u(e)<<" ";//将u作为随机数源,每个调用返回在指定范围内并服从均匀分布的值。
    }
}

uniform_int_distribution<unsigned>类型生成均匀分布的unsigned值,u(0,9)代表我们希望得到0到9之间的数。
分布类型是一个函数对象类,接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布。
值得一提的是,一个给定的随机数生成器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数生成器,应该将其(包括引擎和分布对象)定义为static的,否则每次调用函数都会生成相同的序列。
如果想要随机数生成器每次生成不同的序列,则需要提供种子。种子可以在定义随机数引擎时初始化,也可以在其后调用seed函数。
利用ctime头文件当中定义的time函数,可以返回从一个特定时刻到当前经过了多少秒,用这个数字作为种子是常用的选择:
default_random_engine e(time(0));

生成随机实数 p663
使用uniform_real_distribution类型的对象,可以生成随机浮点数:
uniform_real_distribution<double> u(0, 1);
上面的随机数分布对象可以用于生成0到1之间的double类型随机数。

生成非均匀分布的随机数 p664
例如正态分布:normal_distribution<> n(4, 1.5); //均值4,标准差1.5
更多分布参照 p781。

多重继承 p711
在派生类的派生列表中可以包含多个基类:
class Panda : public Bear, public Endangered { ... };
上例中Panda类继承了Bear类和Endangered类两个类。

虚继承 p717
尽管说在派生列表中同一个基类只能出现一次,但实际上派生类可以多此继承同一个类。
有两种可能性:派生类可以通过它的两个直接基类分别继承同一个间接基类;也可以直接继承某个类然后通过另一个基类再一次间接继承该类。
举个例子,标准库中istream和ostream分别继承了一个共同的名为base_ios的抽象基类;另外iostream类继承了istream和ostream;所以iostream类继承人base_ios两次。
默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中包含该类的多个子对象。
而iostream却不需要两份base_ios的拷贝。解决这种问题就需要用到虚继承机制。
虚继承的目的是令某个类做出声明,承诺愿意共享他的基类。
其中共享的基类子对象称为虚基类。不论虚基类在继承体系中出现了多少次,在派生类都只包含唯一一个共享的虚基类子对象。
使用虚继承的方式是在派生类声明中在要虚继承的基类前加virtual关键字:
class Raccoon : public virtual ZooAnimal { ... }; //public和virtual的位置可调换
class Raccoon : virtual public ZooAnimal { ... }; //public和virtual的位置可调换
virtual代表一种意愿,原理在后续的派生类中共享虚基类的同一份实例。
指定了虚基类后,该类的派生仍然按常规方式进行:
class Panda : public Bear, public Raccoon, public Endangered { ... };
Panda通过Raccon和Bear继承了ZooAnimal,且Panda中只有一个ZooAnimal基类部分。

0 0 投票数
打个分吧!
订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x