• C++ FAQ


    空类

    class A {
    
    };
    // sizeof(A) = 1
    

    空类的大小之所以为1,因为标准规定完整对象的大小>0,否则两个不同对象可能拥有相同的地址,故编译器会生成1B占位符。
    那么两个对象为什么不能地址相同呢?

    There would be no way to distinguish between these two objects when referencing them with pointers.

    空类中到底都有什么呢?

    class A {
    public:
    	A();  // 默认构造函数
    	A(const A&);  // 拷贝构造函数
    	~A();  // 析构函数
    	A& operator=(const A&);  // 赋值运算符
    	A* operator&();  // 取址运算符(非const)
    	const A* operator&() const;  // 取址运算符(const)
    };
    

    仅仅声明一个类,不会创建这些函数。只有当定义类的对象时,才会产生。

    多态和虚函数

    面向对象的语言的特点就是封装、继承和多态。封装和继承都比较好理解,那么多态到底什么意思?
    简单来说:不同对象接收相同的消息产生不同的行为。
    C++中的多态分为静态多态(函数和运算符重载)和动态多态(继承和虚函数)。
    定义虚函数f,是为了用基类的引用或指针调用派生类的f,最终调用哪个f取决于传入的实参,即在运行时选择函数的版本,也就是所谓的动态绑定。

    class Base {
    public:
    	virtual void f() {
    		cout << "Base";
    	}
    	virtual void g() {}
    private:
    	int i;
    };
    
    class Derived : public Base {
    public:
    	virtual void f() {  // 覆盖Base::f
    		cout << "Derived";
    	}
    	virtual void h() {}
    private:
    	int j;
    };
    
    int main() {
    	Base* p = new Derived();
    	p->f();  // 调用派生类的f()
    	delete p;
    	return 0;
    }
    

    基类指针p调用虚函数ff作用的可能是基类对象,也可能是派生类对象,这就是多态(同样消息作用于不同类型对象产生不同的行为)的一种方式,即动态多态。
    正因为编译器无法确定使用哪个虚函数,所以所有的虚函数必须定义,否则编译器会报错。

    构造函数不能是虚函数,因为构造对象时必须明确知道其类型。如果是虚函数,调用时只需要提供接口,编译器无法知道你想构造继承树的哪个类型。
    C++他爹Bjarne Stroustrup是这么说的:

    A virtual call is a mechanism to get work done given partial information. In particular, "virtual" allows us to call a function knowing only an interfaces and not the exact type of the object. To create an object you need complete information. In particular, you need to know the exact type of what you want to create. Consequently, a "call to a constructor" cannot be virtual.

    析构函数是虚函数,因为要确保执行相应对象的析构函数。如果基类指针指向派生类对象,会调用派生类的析构函数,然后调用基类的析构函数。
    在这里插入图片描述

    纯虚函数

    与虚函数必须定义相反,纯虚函数无须定义(要定义必须在类的外部),含有纯虚函数的类是抽象基类
    抽象基类定义好接口,继承该类的其他类可以覆盖这个接口。

    virtual void f() = 0;  // 声明纯虚函数
    

    之所以要引入纯虚函数,是因为很多时候基类产生对象是没有意义的。比如动物类可以派生出狗、猪等子类,但动物类生成对象毫无意义。
    因此,不能创建抽象基类的对象,派生类必须覆盖(override)以定义自己的f,否则派生类仍然是抽象基类。

    重载&覆盖&重写

    • 重载(overload):在类内部发生。函数名相同,参数个数、参数类型、参数顺序至少有一种不同。返回值类型可以相同,也可不同;
    • 覆盖(override):覆盖基类的虚函数。函数名相同,参数相同,基类函数必须是虚函数;
    struct B {
    	virtual void f1(int) const;
    	virtual void f2();
    	void f3();
    };
    
    struct D1 :B {
    	void f1(int) const override;  // 正确:f1与基类中的f1匹配
    	void f2(int) override;  // 错误:B没有形如f2(int)的函数
    	void f3() override;  // 错误:f3不是虚函数
    	void f4() override;  // 错误:B没有名为f4的函数
    };
    
    • 重写(overwrite):派生类的函数屏蔽了同名的基类函数:
      派生类函数与基类函数同名,参数不同。不论基类函数是否为虚函数,都会被隐藏;
      派生类函数与基类函数同名,参数相同。基类函数不为虚函数,会被隐藏;

    static

    C++中static关键字用来声明类的成员

    • 类的静态成员变量或函数属于类而非对象,只有一份副本;
    • 静态成员函数没有this指针,只能访问类的静态数据;
    • 静态成员函数不能定义为虚函数;
    • 静态成员变量初始化int Base::name = 0

    如果不是在类中声明成员,还有下面用法:

    • 隐藏作用:多文件编译时,定义的全局变量和函数都是整个工程可见的,只要使用时加上extern关键字即可。如果加上static关键字,那么该变量或函数就变为仅当前文件可见,这样我们可以在不同文件中定义同名的变量或函数而不用担心冲突。
    • 全局生存期:static变量存储在静态数据区,默认值为0,只被初始化一次,即使作为局部变量,生存期也为整个程序,但作用域与普通变量相同,退出函数后即使变量存在,但不能使用。

    const

    • 定义const对象:一旦创建其值不能改变,故const对象必须初始化。
    const int bufSize = 512;
    int const bufSize = 512;  // the same as the previous one
    

    由于const对象默认只在文件内有效,所以如果要在文件间共享:

    // file1.cpp定义并初始化
    extern const int bufSize = 512;
    // file1.h可以仅声明,不初始化
    extern const int bufSize;
    
    • 常量指针(const pointer):指针本身(存在指针中的地址)不可变。
    int num = 0;
    int* const p = &num;  // p将一直指向num
    
    • 指向常量的指针(pointer to const):指针指向的对象不可变。
    const double pi = 3.14;
    double* p = &pi;  // 错误,p是一个普通指针
    const double* p = &pi;  // 正确
    *p = 4.1;  // 错误,不能改变*p的值 
    
    • 修饰成员函数
    class A {
    	void f() const;  // 不能改变数据成员,const对象不能调用非const成员函数
    };
    
    • 修饰类对象
    class A {
    	void f1();
    	void f2() const;
    };
    
    const A obj;  // obj为常量对象,任何成员都不能被修改,任何非const成员函数都不能被调用
    obj.f1();  // 错误
    obj.f2();  // 正确
    
    const A* obj  = new A();
    obj->f1();  // 错误
    obj->f2();  // 正确
    
    • 转为非const
    const char* pc;  // pc指向内容不可变
    char* p = const_cast<char*>(pc);  // 正确,但是通过p写值是未定义行为
    

    类型转换

    类型转换分为隐式转换和显式转换。
    显式转换有四种:

    • static_cast
      没有底层const都可以,使用比较普遍。
      基类->派生类:不安全
      主要执行非多态转换,代替C中的转换。
    void* p = &d;
    double* dp = static_cast<double*>(p);
    
    • dynamic_cast
      运行时类型检查,
      将基类指针或引用安全转换为派生类的指针或引用:
    // type是类,且有虚函数
    dynamic_cast<type*>(e);  //e是指针
    dynamic_cast<type&>(e);  //e是左值
    dynamic_cast<type&&>(e);  //e不是左值
    
    • const_cast
      改变底层const。
      常量指针转为非常量指针。
    const char* cp;
    char* q = static_cast<char*>(cp);  // wrong, static_cast不能用于底层const
    char* p = const_cast<char*>(cp);  // true
    
    • reinterpret_cast
      比较危险,不太用。处理无关类型转换,重新解释对象的比特模型。

    new/delete/malloc/free

    new/delete是C++运算符,需要编译器支持,所以不需要指定大小,返回相应对象类型的指针,分配失败会抛出std::bad_alloc异常,new会调用operator new()申请内存(用malloc实现),调用构造函数初始化成员变量,返回相应指针,delete先调用析构函数,再调用operator delete()函数释放内存(用free实现);
    malloc/free是库函数,不由编译器控制,需要显式指出大小,返回void*,需要强制类型转换,分配失败返回NULL指针,无法完成对象的构造和析构。

    智能指针

    new完后没有delete,内存泄漏。为了减少程序员的负担,引入智能指针:

    • shared_ptr
      允许多个指针指向同一个对象。通常与make_shared函数结合食用:
    shared_ptr<string> p = make_shared<string>(10, '9');
    

    实现方式一般是reference counting,在堆上申请资源并返回指针后,在堆上申请一个共享的引用计数器,每来一个指针指向该对象,++计数器。当计数器为0时,会自动释放指向的对象。
    2个指针成员,一个指向对象,一个指向计数器
    面试有可能被要求手撕一个:

    template<class T>
    class mySharePtr {
    public:
    	mySharePtr() :refCnt(nullptr), ptr(nullptr) {}
    
    	mySharePtr(T* res) :refCnt(nullptr), ptr(res) {
    		add();
    	}
    
    	mySharePtr(const mySharePtr<T>& p) :refCnt(p.refCnt), ptr(p.ptr) {
    		add();
    	}
    
    	virtual ~mySharePtr() {
    		remove();
    	}
    
    	// lvalue is assigned, --counter
    	mySharePtr<T>& operator=(const mySharePtr<T>& that) {
    		if (this != &that) {
    			remove();
    			this->ptr = that.ptr;
    			this->refCnt = that.refCnt;
    			add();
    		}
    		return *this;
    	}
    
    	bool operator==(const mySharePtr<T>& other) {
    		return ptr == other.ptr;
    	}
    
    	bool operator!=(const mySharePtr<T>& other) {
    		return !operator==(other);
    	}
    
    	T& operator*() const {
    		return *ptr;
    	}
    
    	T* operator->() const {
    		return ptr;
    	}
    
    	int numRef() const {
    		if (refCnt) {
    			return *refCnt;
    		}
    		else {
    			return -1;
    		}
    	}
    protected:
    	// if null, create counter = 1, else ++counter
    	void add() {
    		if (refCnt) {
    			++(*refCnt);
    		}
    		else {
    			refCnt = new int(1);
    		}
    	}
    
    	// --counter, if counter = 0, free memory
    	void remove() {
    		if (refCnt) {
    			--(*refCnt);
    			if (*refCnt == 0) {
    				delete refCnt;
    				delete ptr;
    				refCnt = nullptr;
    				ptr = nullptr;
    			}
    		}
    	}
    private:
    	int* refCnt;
    	T* ptr;
    };
    
    • unique_ptr
      看名字就知道,独占对象。

    指针和引用

    引用只是一个别名,不是一种数据类型,不占存储空间,不能建立数组的引用
    引用必须初始化,指针不必
    引用初始化后不能改变,指针可以改变指向的对象
    不存在指向空值的引用,存在指向空值的指针

    成员变量初始化顺序

    基类静态变量/全局变量:静态成员变量必须类外初始化
    派生类静态变量/全局变量
    基类成员变量:按照在类中定义的顺序,而不是初始化列表中的顺序
    派生类成员变量

    TODO

    初始化列表好处:
    1、const成员变量只能
    2、引用只能
    3、效率:初始化列表比赋值操作少一次默认构造函数,因为程序要默认构造临时对象(等号右边)后才能赋值

    函数缺省:
    某个参数有默认值,缺省参数仍在后边
    调用时如果略去一个参数传递,则略去后面所有

    异常处理:
    抛出异常,没有被特定的catch语句捕获,函数调用堆栈会被解退(函数终止,销毁局部变量,控制权转到调用它的那个函数),
    并在下一个外层try..catch捕获,最后没有任何catch捕获,调用terminate,abort退出。

    传参时传引用与传指针效果相同
    传引用,没有产生实参的副本,直接对实参操作
    传指针,被调函数需要给形参分配空间,可读性差,需要传地址做实参,传引用更简单清晰

    预处理、编译、汇编、链接

    操作系统

    • 用户告诉操作系统执行hello程序

    • 操作系统到硬盘找到该程序

    • 由编译程序将用户源程序编译成若干个目标模块

    • 由链接程序将目标模块和相应的库函数链接成装入模块

    • 操作系统分配内存,由装入程序将装入模块装入内存

    • 为执行hello程序创建执行环境(创建新进程)

    • 操作系统设置CPU上下文环境,并跳到程序开始处

    • 程序的第一条指令执行

    • 程序执行与printf对应的系统调用

    • 操作系统分配设备

    • 执行显示驱动程序

    • 窗口系统将像素写入存储映像区

      (1)每个节点或者是黑色,或者是红色。

      (2)根节点是黑色。

      (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

      (4)如果一个节点是红色的,则它的子节点必须是黑色的。

      (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]

    模板特化、偏特化
    内存池

    volatile:
    加volatile的关键字不进行编译器优化,保证对特殊地址的稳定访问
    不能把他放在cache或寄存器中重复使用
    防止优化编译器把变量从内存装入 CPU 寄存器
    两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行

    拷贝构造:
    创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
    通过使用另一个同类型的对象来初始化新创建的对象。
    复制对象把它作为参数传递给函数。
    复制对象,并从函数返回这个对象。

    map key类型:必须支持<运算符

    数据库缓存一致:
    并发操作导致不一致,本质上修改数据库和删除缓存耦合在一起,使得其他操作有可能读出脏数据
    解决方案:解耦,延迟双删:写->删缓存->修改数据库->延时->再次删缓存
    二:内存队列:写修改数据库,将数据id放入队列,消费者线程消费即可

    浏览器:
    DNS解析(DNS缓存、OS缓存、路由器缓存、递归搜索)、尝试建立TCP连接、发送HTTP请求(get)、服务器处理请求(查询)
    返回HTML,浏览器解析渲染、关闭TCP连接

    线性探测、拉链、再哈希、公共溢出区

    UDP对实时性要求高,数据准确性要求不是太高的场合、视频通话、QQ主要UDP为主,TCP为辅

  • 相关阅读:
    angular的路由例子
    angular自定义module
    docker配置phpadmin需要注意的地方
    linux下钉钉,微信
    debian shell脚本关联
    debian下安装带界面的qemu
    ros的一些设置
    新闻排重方案设计
    细解动态规划(一)
    漫画谈-微积分(二)
  • 原文地址:https://www.cnblogs.com/EIMadrigal/p/12384883.html
Copyright © 2020-2023  润新知