《后台开发核心技术与应用实践》读后感和一些思考

    科技2022-07-16  116

    今天是1024程序员节,先祝大家节日快乐!


    前言:

    最近拜读了腾讯的《后台开发核心技术与应用实践》,其实这本书很早之前就听说过,可以说是“大名鼎鼎”,也可以说是“臭名昭著”。无论是在知乎、掘金或是其他技术论坛上,无论是在学院派、工程派的眼里,他都是一本“培训机构类的书籍”。显然,编程老鸟要么喜欢《C++ prime plus》、《Unix网络编程》这样的事无巨细的大部头,要么喜欢《编程珠玑》、《人月神话》这样能引发长期思考的名著。其实我觉得这本书还是非常有价值,正如作者自己所说,20%的知识会80%地使用,作者正是抓住了这20%的知识进行讲解,剩下的80%还是要靠自己长期地修炼内功!想要成为一名合格的后台开发工程师,还需要长久持续努力!

    目录

    前言:

    strlen与sizeof

    struct字节对齐问题

    判断大小端

    指针与引用的区别

    保证宏定义函数单句展开的do{…}while(0)技巧

    struct和class

    静态数据成员

    静态类成员函数

    多态与虚函数

    虚函数与纯虚函数

    虚函数与析构函数

    this指针

    重载和重写(覆盖)

    单例模式——使用静态局部变量实现

    编译与链接

    参考:


    strlen与sizeof

    sizeof()是运算符,在头文件的类型为unsigned int,其运算值在编译时就计算好了,参数可以是指针、数组、类型、对象和函数等; 具体而言: 数组----编译时分配的数组空间的大小; 指针----存储该指针所用的空间的大小(存储该指针的地址的长度,是长整型,应该是4); 类型----该类型所占的空间的大小; 对象----对象的实际占用空间大小; 函数----函数的返回类型所占的空间大小。函数的返回类型不能是void。strlen()是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化为指针了。该函数完成的功能是从代表该字符串的第一个地址开始遍历的,直到遇到结束符NULL。返回的长度大小不包括NULL。

    举例1:

    char str[20] = “0123456789”; int a = strlen(str); int b = sizeof(str);

    这时的 a = 10,b = 20;因为strlen计算的是字符串的长度,以’\0’为字符串结束标志;而sizeof计算的是分配的数组str[20]所占的内存空间的大小,不受里面存储的内容影响。

    举例2:

    char *str1 = “abcde”; char str2[] = “abcde”; char str3[8] = {‘a’}; char ss[] = “0123456789”; 其计算结果为: sizeof(str1) = 4; sizeof(str2) = 6; sizeof(str3) = 8; sizeof(ss) = 11;

    str1 是一个指针,只是指向了字符串"abcde"而已。所以sizeof(str1)不是字符串占的空间也不是字符数组占的空间,而是一个指针所占的空间。在C/C++中一个指针占四个字节。 str2是一个字符型数组,对于一个数组,返回这个数组所占的总空间,所以sizeof(str2)取得的是字符串"abcde"的总空间。"abcde"中,共有a b c d e \0六个字符,所以str2数组的长度时6,。 str3已经定义成了长度为8的数组,所以sizeof(str3)为8; str4和str2类似,共十一个字符,所以ss所占的空间是11.

    struct字节对齐问题

    1.先确定实际对齐单位,其由以下三个因素决定

        (1) CPU周期

        windows、VS和Qt默认8字节对齐

        Linux 32位默认4字节对齐,Linux 64位默认8字节对齐

        (2) 结构体最大成员(基本数据类型变量)

        (3) 预编译指令#pragma pack(n)手动设置(n--只能填1 2 4 8 16)

        上面三者取最小的,就是实际对齐单位(这里的“实际对齐单位”是我为了方便区分随便取的概念)

    2.除结构体的第一个成员外,其他所有的成员的地址相对于结构体地址(即它首个成员的地址)的偏移量必须为实际对齐单位或自身大小的整数倍(取两者中小的那个)

    3.结构体的整体大小必须为实际对齐单位的整数倍。

    判断大小端

    这个是一道老生常谈的后端面试题。

    首先明确大端和小端的概念:

    大端:低地址存放最高有效位字节。绝大部分网络通信使用的是大端存放方式,所以在后台开发过程中要注意大小端问题。小端:低地址存放最低有效位字节。

    判断的核心思想是利用联合体union都是从低地址开始存放。

    #include<stdio.h> union MyUnion { int a; char c; }; int main() { MyUnion mu; mu.a = 0x0001; if (mu.c == 1) // 因为union都是从低地址开始存放,所以此时肯定为小端 printf("small endian\n"); else printf("big endian\n"); return 0; }

    指针与引用的区别

    指针拥有自己的一块空间,引用只是一个别名。使用sizeof(),指针的大小是4,引用的大小被引用对象的大小。指针可以被初始化为NULL,引用必须被初始化并且必须是一个已有对象的引用。指针可以用多级指针,引用只能有一级。可以用const指针,但是不能用const引用。作为参数传递的时候,指针需要被解引用才可以对对象进行操作,直接对引用进行操作会改变所指向的对象。如果返回动态内存分配的对象或者内存,必须使用指针,引用可能会引起内存泄漏。

    保证宏定义函数单句展开的do{…}while(0)技巧

    在纯C语言的环境下宏定义函数会被经常使用,但是在C++当中,常用其他的方式(例如const static)代替,详细可以参见effective C++

    但是在宏定义函数当中会出现一个问题,试着思考:

    #define help(x) {\ // 执行语句1; \ // 执行语句2; \ }

    这里将help(x)函数拆分成两条语句进行宏定义,看似没有问题,但是如果出现以下的情况时候:

    if(condition) help(x); else ……

    help(x)会被自动展开为两条语句,此处if后面有没有接着大括号{}(可见统一标准编程风格习惯还是很重要的),就会出错!

    所以将上面的宏定义函数定义成:

    #define help(x) do{\ // 执行语句1; \ // 执行语句2; \ } while(0)

    就可以完美解决刚才出现的问题,算是一个常用的小技巧。

    struct和class

    struct中的成员默认的访问权限是public,class中成员的默认访问权限是private。在C语言中,struct不能定义成员函数;在C++当中,struct能够定义成员函数。

    静态数据成员

    static的作用在面试当中非常常见,可以分成全局静态变量、局部静态变量、静态函数、静态数据成员、静态函数成员,面试的时候分成这几个点一一描述即可,这里详细讨论一下静态数据成员变量。

    类中的static成员变量被单独分配一个单一的内存空间,不占用对象的存储空间。静态数据成员被所有对象共享,包括类的派生类对象。即对于多个对象来说,静态数据成员仅存储一处,供所有对象使用。静态数据成员出现必须被定义,而且必须在类外初始化,因为static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化。因为static是所有对象共享的变量,必须要比对象先存在。仅仅声明编译器会报错。由于以上的特性,静态数据成员实际上提供了相互通信的方式。静态数据成员在提供对象之间通信的同时,不会破坏隐藏性原则,即保证了安全性。

    静态类成员函数

    由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员。

    一种必须要用static类成员函数的情况:

    在LeetCode.1356当中,具体题目有不再赘述了,直接看代码:

    class Solution { public: vector<int> sortByBits(vector<int>& arr) { sort(arr.begin(), arr.end(), cmp); return arr; } // 必须要使用static类函数 int bitNum(int num) { int temp = 0; while (num) { if (num % 2 == 1) ++temp; num /= 2; } return temp; } bool cmp(const int& x, const int& y) { int numx = bitNum(x); int numy = bitNum(y); return numx != numy ? numx < numy : x < y; } };

    sort()函数中要使用static类成员函数作为输入函数,上面的写法会出现报错:

    为什么必须要使用static呢?其实这并不是sort函数规定的,而是所有的的普通类成员函数,都不能以函数指针的方式作为其他函数的入口参数,因为普通成员函数在编译阶段,会自动添加了入口参数,这样这个函数指针的模板其实就改变了。

    改成下面就能顺利AC了:

    class Solution { public: vector<int> sortByBits(vector<int>& arr) { sort(arr.begin(), arr.end(), cmp); return arr; } // 必须要使用static类函数 static int bitNum(int num) { int temp = 0; while (num) { if (num % 2 == 1) ++temp; num /= 2; } return temp; } static bool cmp(const int& x, const int& y) { int numx = bitNum(x); int numy = bitNum(y); return numx != numy ? numx < numy : x < y; } };

    多态与虚函数

    十个月之前整理过一次:编程基础——虚函数 explicit 笔记。这次读了之后还是会有一些新的理解,再写一点。

    多态性:具有不同功能的函数具有同一个函数名。这样就可以用一个函数名调用不同内容的函数。换句话说,每个对象可以使用自己的方式去响应共同的消息。

    虚函数是多态的具体实现形式。虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。

    下面举一个通过基类指针访问派生类中同名函数的例子(重写)

    #include <iostream> #include <string> using namespace std; class Box{ public: Box(int, int, int); virtual void display(); //定义成虚函数 protected: int length, height, width; }; // 构造函数初始化 Box::Box(int l, int h, int w) { length = l; height = h; width = w; } // 父类实现虚函数 void Box::display() { cout << "length: " << length << endl; cout << "height: " << height << endl; cout << "width: " << width << endl; } // 派生类,继承Box class SonBox : public Box { public: SonBox(int, int, int, int, string); virtual void display(); private: int weight; string name; }; // 构造函数使用初始化参数列表进行初始化 SonBox::SonBox(int l, int h, int w, int we, string n) : Box(l, h, w), weight(we), name(n) {} // 派生类重写display函数 void SonBox::display() { cout << "length: " << length << endl; cout << "height: " << height << endl; cout << "width: " << width << endl; cout << "weight: " << weight << endl; cout << "name: " << name << endl; } int main() { Box box(1, 2, 3); SonBox sonBox(4, 5, 6, 7, "virtual"); // 此指针指向的基类对象 Box *pt = &box; pt->display(); cout << "----------override the virtual function:------------" << endl; // 指针的类型是基类指针,仍然使用基类指针,但是指向的对象改成了子类对象。 // 也可以说是基类指针指向的是派生类的基类部分。 pt = &sonBox; pt->display(); return 0; }

    运行效果:

    这里要特别注意的是:定义的指针类型一定要是基类指针!如果定义一个子类指针,不是多态!

    回想一下多态的精髓,就是要通过同一个消息,对不同对象有不同的响应方式。所以,上面的例子中,本来基类指针是指向基类对象的,如果用它指向派生类对象,则需要进行指针类型转换,即将派生类对象的指针先转换成激烈的指针,所以基类指针指向的是派生类对象中的基类部分。

    比如将main函数中改成重新定义一个子类指针,不是多态!!!只是普通的函数调用!

    int main() { Box box(1, 2, 3); SonBox sonBox(4, 5, 6, 7, "virtual"); // 此指针指向的基类对象 Box *pt = &box; pt->display(); cout << "----------override the virtual function:------------" << endl; // 如果是重新定义一个子类指针,不是多态!!!只是普通的函数调用 SonBox *newpr = &sonBox; newpr->display(); // pt->display(); return 0; }

    虚函数与纯虚函数

     

    虚函数与析构函数

    将虚函数应用在析构函数当中可以说是最常见的一种一种应用场景了。

     

    this指针

    写Python过来的程序员对this指针肯定了如指掌,C++当中其实也有this指针,只不过经常是隐式存在,所以容易被忽略。

    在每一个成员函数当中都包含有一个特殊的指针,这个指针的名字固定,称为this指针,指向本类对象的指针。指针的值是当前被调用的成员函数所在对象的起始地址。this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。

    比如对于stu.show();,stu 就是当前对象,this 就指向 stu。

    注意:this指针是隐式使用的,作为参数被传递给成员函数,比如:

    int Box::volume(){ return (height * width * length); }

    会被C++处理成:

    int Box::volume(Box *this) { return (this->height * this->width * this->lenght); }

    这些操作都是编译器自动实现的,一般情况下不必人为地增加this指针。关于this的总结如下:

    只能在成员函数中使用,在全局函数、静态成员函数中都不能使用。this指针在成员函数开始前构造,在成员函数结束后清除。this指针会因编译器不同有不同的存储位置,可能是栈、寄存器或全局变量。this是类的指针。因为this指针在成员函数中才有定义,所以获得一个对象后,不能通过对象使用this指针,所以也就无法知道一个对象的this指针位置。不过,在成员函数中可以指定this指针位置。 #include <iostream> using namespace std; class Box{ public: Box(){;} ~Box(){;} Box* get_address() //得到this的地址 { return this; } }; int main(){ Box box1; Box box2; // Box* 定义指针p接受对象box的get_address()成员函数的返回值,并打印 Box* p = box1.get_address(); cout << p << endl; p = box2.get_address(); cout << p << endl; return 0; }

    重载和重写(覆盖)

    重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

    重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

    单例模式——使用静态局部变量实现

    作为设计模式中最基础的一种,单例模式的作用是保证整个程序生命周期中的任何一个时刻,单例类的实例只存在一个。

    #include <iostream> class Singleton { public: ~Singleton(){ std::cout<<"destructor called!"<<std::endl; } Singleton(const Singleton&)=delete; Singleton& operator=(const Singleton&)=delete; static Singleton& get_instance(){ static Singleton instance; return instance; } private: Singleton(){ std::cout<<"constructor called!"<<std::endl; } }; int main(int argc, char *argv[]) { Singleton& instance_1 = Singleton::get_instance(); Singleton& instance_2 = Singleton::get_instance(); return 0; }

    编译与链接

    源程序->预处理->编译和优化->生成目标文件->链接->可执行文件

     

    参考:

    https://blog.csdn.net/lm409/article/details/68569972https://blog.csdn.net/songjiasheng1314/article/details/88644827《后台开发核心技术与应用实践》《Linux高性能服务器编程》牛客C++面经整理https://blog.csdn.net/cherrydreamsover/article/details/81627855https://www.runoob.com/cplusplus/cpp-this-pointer.html

    顺便感谢西电图书馆提供纸质书

     

    Processed: 0.009, SQL: 8