后台开发1-3章读书笔记

前言

     本来想着再开一个仓库的,但是感觉乱七八糟的仓库开太多了也不是特别好,就直接加到博客里面了。

chapter 1 c++ 基础

     再次吐槽一下,没有vim8是真的难用,权当锻炼自己手速了。

  • 使用unio来进行判断自己机器是big endian还是small endian,一般来说网络传输之中都采用的是big endian。
  • 另外一个就是内存对齐,觉得书里面讲得不够详细,可以去看知乎这篇帖子,主要提到了一个#pragma pack(n)的问题,文中好像出现了一个错误就是pack应该是取最大成员和默认pack的较大值,主要还是为了取址方便,进行计算的时候相当于先按照对齐单位去取,要是一个对其单位大小的空间不够的话,就会增加一个对齐单位大小的空间。\

有一个很有意思的例子是使用union和struct相结合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
typedef union{
long i;
int k[5];
char c;
}UDATE;
struct data{
int cat;
UDATE cow;
double dog;
}too;
UDATE temp;
int main(){
cout<<sizeof(too)<<endl;
cout<<sizeof(temp)<<endl;
return 0;
}

答案是40和24,自己可以稍微想一想。

预处理

     主要就是四个功能,宏定义,文件包含,条件编译和布局控制。

  • 宏定义;本质上属于替换,主要有两种形式,一种是直接替换,像#define PI 3.1415926这种,另外就是就是类似于函数那种,如#define area(x) ((x)*(x)),写宏定义的时候需要注意的点就是要多加括号,该加括号的地方一个也不能少,否则在替换的时候会产生意想不到的结果。
  • 这里重点讲一个do while(0)的妙用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define Foo(x){
    //statement one
    //statement two
    }

    if (condition)
    Foo(x);
    else
    ...
    如果这样写的话就会else就会和前面的语句孤立起来,单独的else是不可以直接存在的,所以会产生错误,这时候采用另外一种写法。
    1
    2
    3
    4
    #define Foo(x) do{\
    //statement one
    //statement two
    }while(0)
    并且要注意的是这里是没有括号的。
  • 条件编译
    1
    2
    3
    4
    5
    #ifdef <something>
    //progma
    #else
    //another progma
    #endif
    比如下面这种:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<iostream>
    #include<cstdio>
    using namespace std;
    //#define _DEBUG_ 1
    int main(){
    int x = 10;
    #ifdef _DEBUG_
    cout<<"File:"<<__FILE__<<",Line:"<<__LINE__<<",x:"<<x<<endl;
    #else
    printf("x = %d\n",x);
    cout<<x<<endl;
    #endif
    return 0;
    }
    这要前面有这个_DEBUG_的宏定义,不管定义成啥,甚至后面都没有东西都行,就会执行第一段代码。
  • extern C
    主要原因是因为函数重载所引起的,c++里面重载函数在进行汇编的时候会对函数名字进行一些额外处理,而在C语言中只是简单的函数名字而已,加入extern C的目的是为了告诉编译器这段代码按照C的标准进行编译。另外一个要注意的点就是C++程序的预定义宏__cplusplus。

Chapter 2 面对对象

  • 编码规范,将private成员放到类的最后,就能使读者的精力集中在被外界调用的公有成员上面。
  • 访问权限就不用多说,public,private,protected,这个都是老生常谈了,除了定义访问权限之外,还要注意接口和实现的分离,一个比较经典的就是头文件进行声明,源文件进行实现。注意如果多添加了其他的源文件,在最后编译的时候要链接那个文件
  • 默认构造函数参数所带来的的歧义,有的时候如果所有的构造函数都有参数,再定义默认(无参)构造函数,或者参数个数小于该构造函数的构造函数,调用的时候会引起歧义。
  • 析构函数,一般来说栈中分配内存的对象,在作用域结束的位置就会自动调用析构函数,但是new分配,就是动态内存分配的对象,只有在调用delete之后才会调用析构函数。
  • 静态数据成员,静态数据成员相比全局变量的一大优点就是限制了访问权限,另外一个特点就是静态数据成员有可能会被重复定义,所以一般采用的是头文件声明,源文件进行定义,一定要在类外进行一次定义。最大特点就是无论创建多少个对象,或者派生类产生了多少个对象,所有对象都只占有一个静态数据成员。与之类似,函数中声明的静态变量,在函数结束时并不释放,而是等到整个程序结束时才进行释放。
  • 静态成员函数,静态成员之所以只能操作静态数据成员,是因为静态成员函数不属于任何一个对象,所以函数参数中也没有this指针,也就没办法访问非静态数据成员。
  • 对象所占用空间,这个主要主义的就是两点,一个是内存对齐,另外一个就是虚指针所占空间,指针在64机器所占大小为8个字节。静态成员和成员函数不占用空间。其中空类大小为1的原因是为了避免两个类对象具有相同地址。
  • 关于this指针,这个其实在深入探索对象模型里面又说到过,就是在调用成员函数的时候会隐式传入this指针,通过指针去访问每个对象的成员。
  • 类模板,多个类的功能类似,仅仅是数据类型不一样的时候,就可以用到模板。有一点需要注意的是类外进行模板类的成员函数定义,还要再次添加模板声明。
  • 构造函数和析构函数的调用时机。如果内存位置在栈中,想像一下栈的空间结构,先进后出,所以最晚构造的对象最先调用析构函数,最早构造的对象最晚调用析构函数。全局对象则是等待整个程序快要结束时进行析构函数的调用。

继承与派生

     继承的方式有public,private以及protected。派生类有一个缺点就是他会全盘继承基类的所有成员,即使有的成员用不到也要全盘复制,这样就容易造成数据的冗余,所以要根据派生类的需要谨慎选择基类。

  • 关于基类访问权限,不仅和原本的权限修饰符有关,而且还和继承方式有关。
  • 主要记住一点就好,就是基类的私有成员派生类都不可以访问,然后public继承原有public和protected成员的权限都不会改变,protected继承之后原有public和protected成员都变成protected,private继承原有public和protected成员都会变成private权限,当然基类的私有成员还是不可以访问。
  • 派生类构造函数需要注意的一些点,成员对象以及基类的构造函数参数的初始化必须在初始化列表中进行。
  • 派生类是无法继承基类的析构函数的,所以在派生类中如果有必要需要需要重新定义析构函数来对派生类新增的数据成员进行清理,当调用子类的析构函数时,编译器会自动调用成员对象和基类的析构函数。
  • 关于派生之中各种构造函数调用的顺序,先是最上层的基类调用构造函数,然后是对象成员调用,最后才是派生类调用自己的构造函数。

类的多态

虚函数是C++实现多台的一个非常重要的特性。

Chpter3 常见STL的使用

string

  • 这里有一点之前确实还不是特别清楚,比如说下面这一段代码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include<iostream>
    using namespace std;
    int main(){
    char str[12] = "Hello";
    char *p = str;
    *p = 'h';
    char *ptr = "Hello";
    *ptr = 'h';//这里是不可以改变的
    cout<<str<<endl;
    cout<<ptr<<endl;
    return 0;
    }

    这里在不同的编译器上会有不同的表现,我在本机上跑运行出来会有bus error,但是再leetcode的playground上面跑可以出结果,后者和书本上所描述的一致,就是最后输出str的时候第一个字母并没有小写,所以c primer plus上面说的是对的,就是常量还有静态变量会放在一个地方,这些值是不允许被修改的,str数组可以被修改的原因是数据将常量池中的值复制到了数组之中。*甚至在clang编译器中已经明令禁止了这种行为,converting string literal to char * is depreciated!*

  • string的底层实现其实也可以由指针来完成,其中构造函数还有一些运算符重载的实现要是没有印象了可以再去看看书,这里大致过一遍就可以了。
  • C++字符串和C字符串的转换,需要注意的是,c++字符串并不以‘\0’结尾,data()以字符数组形式返回内容,c_str()返回以’\0’结尾的字符输出,copy()则是写入既有的c_string或字符数组之中。
  • 这里c_str返回的是一个字符数组,如果string的内容被改变,所返回指针所指向内容也会改变,比如下面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<iostream>
    #include<string>
    #include<cstring>
    using namespace std;
    int main(){
    char *cstr = new char[20];
    string str = "Hello world.";
    //const char *cstr = str.c_str; 这样写的话就不string内容被修改之后cstr指向的值也会改变。
    strncpy(cstr,str.c_str(),str.size()+1);
    cout<<cstr<<endl;
    str = "Abcd";
    cout<<cstr<<endl;
    return 0;
    }
    注意在这里,不同编译器表现结果是不一样的,如果去掉size后面的+1,那么g++上就会AddressSanitizer,+1之后就没事了,遇到这种报错一般都会和越界问题有关,自己debug的时候要注意点。

vector

map

  • 其实map也算平常用的比较多的关联型容器了,对于key类型唯一的约束就是必须支持<操作符。内部由红黑树实现,非严格意义平衡二叉树,这棵树具有自动排序的功能,所以map内部的数据都是有序的。

  • map的插入,关于map的插入主要有三种形式,insert插入pair数据,insert插入value_type数据,最后一种就是较为常见的直接用数组方式插入数据,具体例子如下。

    1
    2
    3
    4
    map<int,string> mapStudent;
    mapStudent.insert(pair<int,string>(1,"student_one"));
    mapStudent.insert(map<int,string>::value_type (2,"student_two"));
    mapStudent[3] = "student_three";
  • 关于insert和使用数据方式的区别,有一个是当map中存在这个key的时候,insert是无法继续插入数据的,但是使用数组方式可以覆盖之前的数据。重新插入值是否成功可以通过返回的pair第二个值来判断,true为插入成功,false为插入失败,例子代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include<iostream>
    #include<string>
    #include<map>
    using namespace std;
    int main(){
    map<int,string> mapStudent;
    mapStudent.insert(pair<int,string>(1,"student_one"));
    mapStudent.insert(map<int,string>::value_type(2,"student_two"));
    mapStudent[3] = "student_three";
    auto p1 = mapStudent.insert(pair<int,string>(1,"student1"));
    auto p2 = mapStudent.insert(map<int,string>::value_type(2,"student2"));
    mapStudent[3] = "student3";
    cout<<p1.second<<" "<<p2.second<<endl;
    for(auto item:mapStudent){
    cout<<item.first<<" "<<item.second<<"\n";
    }

    return 0;
    }
  • map的遍历

    三种方式,前向迭代器遍历,反向迭代器遍历,数组方式遍历。

    1
    2
    3
    4
    5
    >//反向遍历
    >map<int,string>::reverse_iterator iter;
    >for(iter = mapStudent.rbegin();iter!=mapStudent.rend();++iter){
    //ur code here
    >}

    前向迭代器和数组方式遍历都很简单,这里就不多说了。

  • map查找数据
    主要有两种方式,一种是count,一种是find,后者可以返回需要查找元素的迭代器,如果没有找到则返回end()迭代器。注意两者都是按key来查找

  • map的删除
    这里不建议用书中的代码来进行删除,因为c++ primer里面有提到过,不要在循环里面进行删除。还有一点需要注意的就是,erase的返回值是删除元素后面一个迭代器。

  • map的排序
    普通情况下是按照key从小到大排序。但是添加第三个模板参数为greater时可以按照key从大到小进行排序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<iostream>
    #include<string>
    #include<map>
    using namespace std;
    int main(){
    map<string,int,greater<string>> mapStudent;
    mapStudent["LiMin"] = 90;
    mapStudent["ZiLinMi"] = 72;
    mapStudent["BoB"] = 79;
    for(auto iter = mapStudent.begin();iter!=mapStudent.end();++iter){
    cout<<iter->first<<" "<<iter->second<<endl;
    }
    return 0;
    }

    这种写法有点像优先队列中中模板的第三个参数。template<class key, class T, class Compare = less<Key>,class Allocator = allocator<pair<const Key,T> > > class map;,所以我们要重新定义map内部排序顺序,只需要再定义一个Compare模板即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include<iostream>
    #include<string>
    #include<map>
    using namespace std;
    struct Cmp{
    //一定一定要注意这个const,不用这个const就会报一大堆错,原因我感觉是
    //排序的时候取出的是string的copy,算是literal,如果不支持string literal
    //的比较,就会出错。
    bool operator()(const string& k1,const string& k2)const{
    return k1.size()<k2.size();
    }
    };
    int main(){
    map<string,int,Cmp> mapStudent;
    mapStudent["LiMin"] = 90;
    mapStudent["ZiLinMi"] = 72;
    mapStudent["BoB"] = 79;
    for(auto iter = mapStudent.begin();iter!=mapStudent.end();++iter){
    cout<<iter->first<<" "<<iter->second<<endl;
    }
    return 0;
    }

    将map按照value进行排序,首先排除sort,因为sort只能对顺序容器进行排序,所以一个可行的方案就是将map中的元素储存到vector之中再调用sort,必要时要自定义compare函数。

  • 内部实现
    就是人尽皆知的红黑树,这里只列出红黑树的一些基本性质,根节点和叶子节点下的空节点是黑色,如果一个节点是红的,那么他的子节点必定是黑色,从根节点到任意一个子节点的路径上的黑色节点数目是相同的。

    set

    set的定义就不多说了,这里强调几点set的特性。

  1. map和set的插入删除效率要比其他顺序容器要高,因为顺序容器再进行元素添加的时候可能会遇到扩容而重新转移内存位置而进行再次拷贝的情况,但是map和set内部实现使用的是红黑树,其结构形式和链表类似,所以不会进行原有内容的移动。
  2. 此外由于原来位置的元素内存不会移动的缘故,每次insert之后,以前保存的迭代器不会失效。
  3. 树形结构下,查找效率相对来说也比较高,每增加一倍的元素查找次数平均只增加一次。
  • 具体API的是实现就不在这里赘述了。
作者

Jason Heywood

发布于

2020-09-28

更新于

2020-10-14

许可协议