0%

c++ review

Accelerated c++

使用字符串

  1. >>运算符从标准输入读进一个字符串,它会首先略去输入开始碰到的空白字符(空白、制表符、回退键和换行符),然后连续的读入字符到某个变量中,直到遇到了另一个空白字符或文件结束标记为止

  2. c++的数据结构可以用struct实现

    1
    2
    3
    4
    5
    struct studentInfo{
    string name;
    double midterm;
    vector<double> homework;
    };
  3. 随机访问:s[2],s[5],这样随机的数字进行访问就是随机访问,顺序访问就是从第一个元素开始,按照既定顺序一个一个遍历到结尾

  4. 半开[i,j)区间的元素个数是j-i,[i,j]闭区间的元素个数是j-i+1

  1. 反向迭代器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include<vector>
    using namespace std;
    int main()
    {
    vector<char> v{'j','a','v','a'};
    vector<char>::reverse_iterator rtr;
    for(rtr=v.rbegin();rtr!=v.rend();rtr++)
    std::cout<< *rtr;
    return 0;
    }

    输出:

    1
    avaj
  2. 如果我们在类的内部定义了一个成员函数,就是请求把对这个函数的调用扩展成内联子过程,这样就避免了函数调用额外的开销。如果成员函数是在类的外部定义的,那么我们必须在这个函数的函数名中指明他是来自类作用域的:"class-name::membername"

  3. 如果在第一个保护标识符之前有成员,class中的成员是私有的(private),struct中的成员是共有的(public)

C++ Primer

Part1也就是前七章,除了6.6,6.7节,都要通读。尤其是第三章初步介绍了vector和string,简直就是新手福音,搞定这两个容器就能写一些简单的程序。
Part2基本就是数据结构和算法,如果有基础读起来很轻松。
9,11两章介绍的容器,以及12.1节的智能指针要通读。多用智能指针和容器,远离segment fault. 第10章里的泛型算法可以慢慢读,读完以后可以写出高逼格的函数式风格C++。12.2节讲了怎么用new和delete分配空间,题主作为新手,知道这种写法就行,写程序时尽量用容器代替原始数组,尤其是代码里最好不要有delete。
Part3是块硬骨头,标题就是Tools for Class Authors. 作为一个”class user”,有些部分第一次是可以略过的。
13章很重要,要细读。初始化,复制,赋值,右值引用是C++里很微妙很重要的部分,别的语言对于这些概念很少有区分得这么细的。这一章不但要精读,还要完全掌握。
14章的操作符重载第一次可以观其大略;14.9节第一次可以跳过。
15章讲OOP,重要性不言而喻。如果之前一点概念都没有,学起来会觉得比较抽象。网上关于OOP有很多通俗有趣的文章,可以一起看看。
16章讲泛型编程,第一次读16.1节,掌握最基本的函数模板和类模板就行了。
Part4就更高档了,很多内容第一次就算啃下来,长久不用又忘了。第一次读推荐把18.2节读懂,命名空间简单易用效果好。别的内容可以观其大略,用时再看。17.1节的tuple是个有趣的东东,可以读一读。17.3节的正则表达式和17.4节的随机数也许有用,也可以读一读。如果需要读写文件,要读一下17.5.2节的raw I/O和17.5.3节的random I/O。

最后给题主的建议是,写C++,要尽量避免C的写法。用static_cast而不是括号转换符;用vector而不是C里面的数组;用string而不是char *;用智能指针而不是原始指针。当然I/O是个例外,printf()还是比cout好用的;转换数字和字符串时sprintf()也比stringstream快

  1. 如果想声明一个变量而非定义它,就在变量名前加关键字extern,定义与声明的区别:定义申请存储空间,也可能会为变量赋一个初始值

  2. 头文件不应该包含using声明

  3. 顶层const表示指针本身是个常量,底层const表示指针所指对象是个常量

  4. 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内

  5. 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值

  6. 将只有一个参数的构造函数声明为explicit可以抑制构造函数定义的隐式转换,只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复

  7. 要想确保对象只定义一次,最好的办法是把静态数据成员的定义和其他非内联函数的定义放在同一个文件中

  8. lambda表达式

    image-20220715200844207

  9. 11.4节的hasher函数hash<string>()(sd.isbn());hash()生成了可调用对象,然后该可调用对象(可以看作是函数)作用在sd.isbn()上

  10. 返回const和非const的函数重载,是因为调用函数时隐式的传递了this指针

  11. image-20220716144342905

  12. 用new分配数组时值初始化和默认初始化,默认情况下动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化.也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可

  13. 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

  14. 一旦一个类需要析构函数,那么它几乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符.一旦一个类需要一个拷贝构造函数,它一定也需要一个拷贝复制运算符,但不一定需要析构函数

  15. 拷贝赋值运算符应该返回一个指向其左侧运算对象的引用,即在赋值函数中return *this

  16. 通常情况下,不应该重载逗号、取地址、逻辑与、和逻辑或运算符

  17. 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做,这两类运算符都应该返回左侧运算对象的引用

  18. 如果类定义了调用运算符,则该类的对象称作函数对象,因为可以调用这种对象,所以我们说这些对象的行为像函数一样

  19. 赋值(包括复合赋值)、下标、函数调用和箭头运算符必须作为类的成员

  20. 在c++中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定(又称作运行时绑定)

  21. 派生类构造函数只初始化它的直接基类

  22. 派生列表中用到的访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见

  23. explicit关键字:指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化

  24. 必须使用初始化列表的时候:

    (1)常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
    (2)引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

    (3)没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

    下图解释了拷贝构造函数

    image-20220907104527530

image-20221028221748699

  1. 构造函数的调用:

image-20220907115924242

  1. 一个左值是一个标识非临时性对象的表达式。一个右值是一个标识临时性对象的表达式,或者一个不与任何对象相联系的值(如字面值常数),一般的法则是,如果程序中有一个变量名,那么他就是一个左值。在所有情况下函数调用的结果都是右值。

  2. 当一个类包含指针作为数据成员时,一般我们必须自己实现析构函数,拷贝赋值函数和拷贝构造函数

    image-20220907150933740

B站C++视频课程

1. 引用

1.1 引用的本质

给变量取个别名。定义引用时必须初始化,因为系统不会为引用开辟空间,引用和原变量代表同一内存空间。c语言角度看底层用常量指针实现(用c++不要关心这件事)。

1
2
3
4
int a=10;
int &b=a;
//底层实际发生的是
//int * const b=&a;

1.2 引用赋值问题

image-20220914141348979

1.3 函数返回值为引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int function1(int & aa)
{
return aa;
}
int & function2(int & aa)
{
return aa;
}
int main()
{
int a = 10;
int b;
b = function1(a);//function1()的返回值先储存在一个临时变量中,
//然后再把临时变量赋值给b
function1(a) = 20;//不OK function1()的返回值为临时变量,不能赋值
function2(a) = 20;//OK 此时a的值变成了20,返回的是左值引用可以出现在等号左边
}

说明:若函数的返回值为引用(&),则编译器就不为返回值创建临时变量了。直接返回那个变量的引用。所以千万不要返回临时变量的引用,如下:

1
2
3
4
5
6
7
8
9
10
11
int & function()
{
int b = 10;
return b;//不OK 等函数返回后,b就消失了,引用了一个消失的东西
//程序会懵逼的。指针也一样。
}
int main()
{
int a;
a = function();//function()返回的东西已经消失了,引用也就不存在了
}

image-20220914142131245

2. 内联函数

image-20220913134523934

编译阶段将内联函数中的函数体替换函数调用处,避免函数调用时的开销(入栈出栈)。必须在定义时使用关键字inline修饰,不能在声明的时候使用inline

3. 缺省参数

建议在函数声明处给默认值。

4. 构造函数

调用构造函数的几种方式

1
2
3
4
5
6
7
8
9
10
//隐式调用无参构造(推荐)
Data ob1;
//显示调用有参构造
Data ob2=Data()
//隐式调用有参构造(推荐)
Data ob3(10);
//显示调用有参构造
Data ob4=Data(10)
//构造函数的隐式转换(前提:类中只有一个数据成员)
Data ob5=30;//Data ob5(30)

5. 析构函数

  • 如果类有指针成员必须实现析构函数,释放指针成员所指向的内存空间。

  • 同一级别先构造的后释放(符合栈的特点)

6. 拷贝构造函数

6.1 定义

本质是构造函数,只有在旧对象初始化新对象(定义时的对象是新对象,定义完以后就变成了旧对象)时才会调用拷贝构造。函数调用时给形参(此时的形参是普通对象,不是引用)开辟空间,会调用拷贝构造函数用实参初始化形参。

6.2 需要自己实现的情况

如果类中没有指针成员,不用实现拷贝构造(浅拷贝)和析构函数;如果类中有指针成员,必须实现拷贝构造(深拷贝)和析构函数(释放指针成员指向的堆区空间)

6.3 拷贝构造的几种调用形式

  • 旧对象给新对象初始化,调用拷贝构造
  • 给对象取别名,不会调用拷贝构造
  • 普通对象作为函数参数,调用函数时,会发生拷贝构造

7. 初始化列表

  • 成员对象:一个类的对象作为另一个类的成员。

  • 类会自动调用成员对象的无参构造函数,如果类中想调用成员对象的有参构造函数,必须使用初始化列表。

  • 对象的初始化和析构顺序:先初始化类中的成员对象(先把内部建好才能建外部),然后再初始化该对象;析构顺序正好相反(先拆外部才能拆内部),先析构该对象,再析构成员对象。

  • 对象数组的初始化:必须显示使用有参构造,逐个元素初始化。若不显示初始化,则会对每个元素默认调用无参构造函数

8. 类的存储结构

成员函数、静态成员是独立存储,所有对象共享的,对象独有的只有成员数据

9. this指针

10. 类中有指针成员

如果有指针成员,必须自己写拷贝构造函数、赋值运算符函数、析构函数

11. 重载箭头运算符

箭头操作符(->)的内置用法是,使用一个类对象的指针来调用所指对象的成员。左操作数为对象指针,右操作数为该对象的成员。

定义重载箭头操作符之后看起来就有点特别,既可用类对象的指针来调用,也可用类对象直接调用。
重载箭头操作符,首先重载箭头操作符必须定义为类成员函数。

箭头操作符与众不同。它其实是一元操作符,却表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。

这里没有第二个形参,因为 -> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。

对于形如point->member的表达式来说,point必须是二者之一:指向类对象的指针、一个重载了operator->() 的类对象。

根据point类型的不同,有如下两条作用规则:

  1. 如果point是指针,则按照内置的箭头运算符去处理。表达式等价于(*point).member。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类没有名为member的成员,则编译器报错。

  2. 如果point是一个定义了operator->() 的类对象,则point->member等价于point.operator->() ->member。其中,如果operator->()的返回结果是一个指针,则转第1步;如果返回结果仍然是一个对象,且该对象本身也重载了operator->(),则重复调用第2步,否则编译器报错。最终,过程要么结束在第一步,要么无限递归,要么报错。

也就是说,如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

class firstClass {
public:
firstClass* operator->() {
std::cout << "firstClass ->() is called!" << std::endl;
return this;
}
void action() {
std::cout << "firstClass action() is called!" << std::endl;
return;
}
};

class myClass {
firstClass firstObj;
public:
firstClass& operator->() {
std::cout << "myClass ->() is called!" << std::endl;
return firstObj;
}
void action() {
std::cout << "myClass action() is called!" << std::endl;
return;
}
};

int main() {
myClass obj;
obj->action();

return 0;
}

调用结果为:

1
2
3
myClass ->() is called!
firstClass ->() is called!
firstClass action() is called!

12. 继承

12.1 重定义

重定义:有继承,子类重定义了父类的同名函数(包含静态和非静态,必须是非虚函数),参数顺序、个数、类型可以不同,子类一旦重定义了父类的同名函数,不管参数是否一致,都将屏蔽父类所有的同名函数,需要加父类的作用域才能访问。

补充一下重载的定义:同一作用域,同名函数,参数的顺序、个数、类型不同 都可以重载。函数的返回值 类型不能作为重载条件(函数重载、运算符重载)

12.2 子类不能继承父类的成员

父类的构造函数、析构函数(但是在析构函数前加上virtual也有多态的性质,编译器为了让析构函数实现多态,会将它们的名字都处理为destructor)、赋值运算符不能被子类继承。

友元不属于类的成员所以不存在继承。

13. 多态

13.1 多态的分类

  • 静态多态:函数重载,运算符重载。

  • 动态多态:有继承,子类重写父类的虚函数,父类指针指向子类地址空间。

13.2 虚函数

定义:在成员函数前virtual修饰。

子类重写父类的虚函数要注意:函数名、返回值类型、参数类型个数顺序必须完全一致。

今后在定义类时析构函数都定义为虚函数。

重写(覆盖):有继承,子类重写父类的虚函数。返回值类型、函数名、参数顺序、个数、类型都必须一致。

13.3 虚函数的实现机制

靠虚函数指针和虚函数表实现

image-20220914211210548

14. STL-容器

14.1 vector容器

单端动态数组容器

image-20220916210913071

14.2 deque容器

双端动态数组容器

image-20220916215345053

15. STL-算法