c++基础知识整理与分析
const使用
const 修饰指针的情况,因为p、q本身为地址,地址里面存放着值
//const 在*左边,值不能变,但地址可以变
const int* p = new int(10);
int const * p = new int(10);
p = new int(20);
//const在*右边,说明地址不能变,但值可以变
int* const q = new int(20);
*q = 30;
const 修饰函数
class A
{
public:
const int func1(){} //返回值不能被修改
void func2(const int &a){} //a不能被修改
void func3() const {} //函数内使用的成员变量不能被修改
}
new delete实现原理
new
operator new 与malloc类似,区别在于operator new 在malloc的基础上加了失败抛出异常,
operator new 与new的区别在于,new比operator new多一步调用构造函数。
1.对于基本类型 new->operator new ->malloc
2.对于类 new->operator new ->malloc->构造函数
delete
1.对于基本类型 delete->operator delete->free
2.对于类 delete->operator delete->free->析构函数
placement new
在事先申请的内存上构造一个新的对象。
int *a = new int(10); //*a = 10
new(a) int(20); //*a = 20;
new delete和malloc free的区别
new delete | malloc free | |
---|---|---|
是否需要指定大小 | 0 | 1 |
类型 | 运算符 | 库函数 |
能否重载 | 1 | 0 |
失败返回 | 异常bad_calloc | NULL |
是否调用构造、析构 | 1 | 0 |
返回类型 | 对象类型 | void * |
内存映像
class Base
{
public:
void func1()
{
cout << "func1 call" << endl;
}
static void func2() {}
void func3() {}
private:
int data;
static int s_data;
};
int main()
{
Base *b = nullptr;
b->func1(); //此处调用成功,会打印"func1 call"
}
Base类对应的对象内存映像如下图
static成员变量放在静态区,static和non-static成员函数放在代码段,代码段和静态区只有一份,各类共享。non-static成员变量每个类都有一份。
为什么空的对象指针也能调用成员函数,并且能调用成功?
如上说明,对象与代码段中的函数通过this指针进行绑定,对象能访问成员函数,此处只是将this指针设置为nullptr,而函数实现中并没有使用this,因此能调用成功。
继承关系中构造函数/析构函数调用顺序
在单继承中,构造函数调用关系为 父类构造->子类构造;析构函数调用:子类析构->父类析构
多继承:按照继承顺序,BaseA->BaseB->C;析构函数:C->BaseB->BaseA
class BaseA
{
};
class BaseB
{};
class C:public BaseA,public BaseB
{
};
多态的实现原理
静态多态(编译时多态)
静态多态基于函数重载实现,在编译期间就能确定接口具体调用哪个实现。
函数重载:函数名相同,函数的形参类型或参数个数不同;若形参完全相同,只有返回参数不同,不是函数重载。
函数重写:函数名、形参个数和类型都相同。
不同点
区别 | 重载 | 重写 |
---|---|---|
发生条件 | 同一作用域 | 继承时 |
实际用途 | 扩展实现,提供多个实现版本 | 覆盖实现 |
动态多态(运行时多态)
动态动态基于虚函数实现,子继承父类,并且重写父类的虚函数,在使用时,父类指针指向子类对象则调用子
类虚函数,指向父类对象则调用父类虚函数。由于在运行期间才能决定调用哪个实现,则为运行时多态。
父类指针如何知道该调用父类虚函数还是子类虚函数?
当类中存在虚函数或存在虚继承关系时,编译器会为对象生成一个虚表,虚表前一个位置存放了一个
指向type_info指针的地址,type_info存放了继承关系,对象信息等
虚表:
每个对象都会有虚表,一般存放在对象的首地址,虚表中存放则虚函数的地址,存放顺序为虚函数的定义顺序,
父类对象中的虚表存放的父类的虚函数的地址,子类对象的虚表分为几种情况:
(1)父类中存在的虚函数,子类没重写:存放的还是父类虚函数的地址
(2)父类中存在的虚函数,子类重写:存放的子类虚函数的地址
(3)子类新增的虚函数:虚表中追加新的虚函数的地址
通过代码获取虚表等信息:
class Base
{
public:
void func1()
{
cout << "call func1" << endl;
}
virtual void func2()
{
cout << "call func2" << endl;
}
virtual void func3()
{
cout << "call func3" << endl;
}
};
int main()
{
Base b;
cout << "虚表地址:" << (intptr_t*)(&b) << endl;
auto vptr = *(intptr_t*)(&b); //虚表指针
auto vfuncptr = (intptr_t*)vptr; //虚函数指针
cout << "虚函数地址" << vfuncptr << endl;
typedef void (*f) ();
f fun1 = (f)vfuncptr[0]; //func2,第一个虚函数
f fun2 = (f)vfuncptr[1]; //func3,第二个虚函数
}
设计原则
单一职责
一个类尽量负责单一的任务,减少类的复杂程度,利于扩展和维护。
开闭原则
一个类尽量能扩展,少修改。以扩展的方式进行功能的修改,保证已有功能在增加功能后还是稳定的。
里氏替换原则
所有用到子类对象的地方都能替换为父类对象
子类不能重写父类的非抽象方法。
子类可以扩展新的接口
依赖倒置
高层模块(使用接口的类)不依赖底层模块(接口类),都依赖一个抽象接口类
如下代码,有一个打印类,可以打印其他类中的Wait,目前只有打印书的需求,
若后面增加一个打印报纸的需求,那就得增加或修改Paint类,这样耦合度就
太高了,因为打印类根本不用知道其他类,它只管打印,因此不应该进行变动
class Book{
public:
string Wait()
{
return "this is book!";
}
};
class Paint{
public:
void PaintFunc(const Book &b)
{
cout<<b.Wait()<<endl;
}
};
可以增加一个抽象类,提供一个抽象接口,打印类去依赖抽象接口
class Manage{
public:
virtual string Wait() = 0;
};
class Book :public Manage{
public:
string Wait()
{
return "this is book!";
}
};
class Paint{
public:
void PaintFunc(Manage *b)
{
cout<<b->Wait()<<endl;
}
};
三级目录
设计模式
单例模式
饿汉模式
优点:代码简单,不用考虑释放内存问题
缺点:程序一启动就会加载资源,资源过多会导致程序启动变慢,且多个单例创建顺序无法控制
class SingleInstance
{
public:
SingleInstance() = delete;
~SingleInstance() = delete;
SingleInstance(const SingleInstance&) = delete;
SingleInstance& operator=(const SingleInstance&) = delete;
static SingleInstance* GetInstance()
{
return &s;
}
protected:
private:
static SingleInstance s;
};
懒汉模式
优点:延迟加载,程序启动无影响,且多个单例创建顺序可以控制
缺点:程序一启动就会加载资源,资源过多会导致程序启动变慢
普通懒汉模式(代码简单、非线程安全)
class SingleInstance
{
public:
static SingleInstance* GetInstance()
{
if (s == nullptr)
{
s = new SingleInstance;
}
return s;
}
protected:
private:
SingleInstance() {}
~SingleInstance() {}
SingleInstance(const SingleInstance&) {}
SingleInstance& operator=(const SingleInstance&) {}
static SingleInstance *s;
};
SingleInstance *SingleInstance::s = nullptr;
简单加锁(每次都上锁,影响性能,但线程安全)
class SingleInstance
{
public:
static SingleInstance* GetInstance()
{
single_mutex.lock();
if (s == nullptr)
{
s = new SingleInstance;
}
single_mutex.unlock();
return s;
}
protected:
private:
SingleInstance() {}
~SingleInstance() {}
SingleInstance(const SingleInstance&) {}
SingleInstance& operator=(const SingleInstance&) {}
static SingleInstance *s;
static mutex single_mutex;
};
SingleInstance *SingleInstance::s = nullptr;
mutex SingleInstance::single_mutex;
复杂加锁(只有在第一次创建的时候才会加锁,线程安全)
class SingleInstance
{
public:
static SingleInstance* GetInstance()
{
if (s == nullptr) //第一次判断为了只创建一次
{
single_mutex.lock(); //多个线程同时到达此处,第一个线程上锁,其他线程等待
//第二次判断防止多个线程同时调用时,只创建一次,若没有这次判断,
//第一个线程到这里会创建,然后解锁,第二个等待的线程也会到达这里,再次创建对象,这样第一个
//设置的参数可能在第二个线程创建对象后消失。
if (s == nullptr)
{
s = new SingleInstance;
}
single_mutex.unlock();
}
return s;
}
protected:
private:
SingleInstance() {}
~SingleInstance() {}
SingleInstance(const SingleInstance&) {}
SingleInstance& operator=(const SingleInstance&) {}
static SingleInstance *s;
static mutex single_mutex;
};
SingleInstance *SingleInstance::s = nullptr;
mutex SingleInstance::single_mutex;
单例释放对象
采用内嵌类的方式进行释放
class SingleInstance
{
public:
static SingleInstance* GetInstance()
{
if (s == nullptr)
{
single_mutex.lock();
if (s == nullptr)
{
s = new SingleInstance;
}
single_mutex.unlock();
}
return s;
}
class Garbo {
public:
~Garbo() //释放时调用析构函数
{
if (SingleInstance::s == nullptr)
delete SingleInstance::s;
}
};
protected:
private:
SingleInstance() {}
~SingleInstance() {}
SingleInstance(const SingleInstance&) {}
SingleInstance& operator=(const SingleInstance&) {}
static SingleInstance *s;
static mutex single_mutex;
static Garbo garbo; //程序结束会释放该变量
};
SingleInstance *SingleInstance::s = nullptr;
SingleInstance::Garbo SingleInstance::garbo;
mutex SingleInstance::single_mutex;