c++基础知识
-
栈:非new获取的变量,编译器分配释放
-
堆:new获取,需手动释放
-
全局数据区:全局变量&static变量存储区域,.data存储初始化(包含读写data区、只读数据区),.bss存储未初始化(或初始化为0)内容
-
文字常量区:常量字符串,编译时确定
-
代码区:存储程序代码
函数调用栈分布:
ebp:栈底指针 esp:栈顶指针 eip:寄存器要读的指令地址
ret指令=pop+jump
详解可参考:https://www.cnblogs.com/sddai/p/9762968.html
虚函数、虚表
-
虚表为类所有、全局数据区
-
虚表指针为对象所有
-
构造函数不可为虚函数、析构函数应该为虚函数
多态、继承的内存大小
-
空类大小为1、编译器为空类插入1字节的char,以使该类对象在内存得以配置一个地址
class Base {
};
-
单一继承(
class Base {
public:
void f();
virtual void g(); // 虚表指针,位于类头部、一个虚函数
private:
int a;
static int b; // 类所有,不占空间
char c; // padding加字节让类符合对齐原则
}
32大小为12、64位大小16
class Derived : public Base {
public:
virtual void h(); // 两个虚函数、一个虚表指针(这个虚函数只有子类有、父类没有)
private:
int c;
int d;
}
32位大小为20、64位大小为24
注意:单一继承、派生类与基类公用虚表指针(都位于类对象初始为0的位置)
-
单一虚继承(派生类不新增虚函数)
class Base {
public:
void f();
virtual void g();
virtual void h();
private:
int a;
int b;
};
class Derived : virtual public Base {
public:
virtual void g();
private:
int c;
int d;
}
-
单一虚继承(派生类新增虚函数)
class Base {
public:
void f();
virtual void g();
virtual void h();
private:
int a;
int b;
};
class Derived : virtual public Base {
public:
virtual void g();
virtual void h_derived();
private:
int c;
int d;
};
从上图与单一的虚拟继承(派生类不新增虚函数)相比,Derived object size是28,相比增大了4个byte,而这4个byte正是Derived中被编译器新增安插的vfptr(上图红色框图中)。
-
多继承(派生类不新增虚函数)
class Base {
public:
virtual void virtualFunction();
private:
int a;
int b;
};
class Derived1 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived1Function();
private:
int c;
};
class Derived2 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived2Function();
private:
int d;
};
class Derived : public Derived1, public Derived2 {
public:
virtual void virtualFunction();
private:
int e;
}
-
多继承(派生类新增虚函数)
class Base {
public:
virtual void virtualFunction();
private:
int a;
int b;
};
class Derived1 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived1Function();
private:
int c;
};
class Derived2 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived2Function();
private:
int d;
};
class Derived : public Derived1, public Derived2 {
public:
virtual void virtualFunction();
virtual void virtualDerivedFunction();
private:
int e;
}
我们看到唯一差异点:上图红色框图中,Derived新增的虚函数只有在vftable@Derived1的虚函数表中有,并不会在vftable@Derived2的虚函数表。
-
虚拟多继承(派生类新增虚函数)
class Base {
public:
virtual void virtualFunction();
private:
int a;
int b;
};
class Derived1 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived1Function();
private:
int c;
};
class Derived2 : public Base {
public:
virtual void virtualFunction();
virtual void virtualDerived2Function();
private:
int d;
};
class Derived : public Derived1, public Derived2 {
public:
virtual void virtualFunction();
virtual void virtualDerivedFunction();
private:
int e;
};
最后总结:https://blog.csdn.net/u014558668/article/details/77476448
1.当类含有虚函数时(包括继承而来的虚函数)都有虚函数表指针vfptr和虚函数表vftable。
2.单一的普通继承(非虚继承),
3.多重继承(非虚继承),
4.如果是单一的虚拟继承,则子类对象中会被编译器安插一个指向vbtable(记录virtual base class在内存布局中与vbptr的距离偏移)的vbptr。
5.对于钻石型多重继承,可以按照以上原则类推。
说明:
模板
模板、特化、偏特化,
using namespace std;
// 一般化设计
template <class T, class T1>
class TestClass
{
public:
TestClass()
{
cout<<"T, T1"<<endl;
}
};
// 针对普通指针的偏特化设计
template <class T, class T1>
class TestClass<T*, T1*>
{
public:
TestClass()
{
cout<<"T*, T1*"<<endl;
}
};
// 针对const指针的偏特化设计
template <class T, class T1>
class TestClass<const T*, T1*>
{
public:
TestClass()
{
cout<<"const T*, T1*"<<endl;
}
};
// 特化
template <>
class TestClass<int, int> {
public:
TestClass()
{
cout << "int, int" << endl;
}
}
int main()
{
TestClass<int, char> obj;
TestClass<int *, char *> obj1;
TestClass<const int *, char *> obj2;
return 0;
}
编译器自动生成的函数
class Dog {
// C++03
Dog(); // 默认构造函数
Dog(const Dog&); // 拷贝构造函数
~Dog() // 析构函数
// 拷贝赋值运算符
Dog& operator=(const Dog&);
// C++11 新增
Dog(Dog&&); // 移动构造函数
// 移动赋值运算符
Dog& operator=(Dog&&);
};
c++编译器默认会生成的六种类相关函数
-
默认构造函数:只在用户没有定义其它类型构造函数时才会生成
-
拷贝构造函数:只有用户没有定义
-
拷贝赋值运算符:只有用户没有定义
-
析构函数:无限制
-
移动构造函数:只有用户没有定义
-
移动赋值运算符:只有用户没有定义
static关键字作用
-
静态成员变量:类拥有、存储在全局数据区、类外初始化(sizeof不计算这部分内存)
-
静态成员函数:类拥有、只能访问static变量&函数、无this指针
-
静态全局变量:文件可见(不同文件可同名)、存储在全局数据区
-
静态局部变量:只在声明时初始化一次、存储在全局数据区
-
静态函数:文件可见(不同文件可同名)
const关键字作用
-
const全局变量、局部变量:不可更改、定义时初始化
-
const修饰指针:看const和*的位置,const在左侧:指针所指内容不可变(
-
const修饰函数
-
参数为复制型,无意义
-
参数为引用,不可更改
-
参数为指针,参考const修饰指针
-
修饰返回值,不可更改
-
-
Const类成员变量:不可更改、初始化列表赋值
-
Const类成员函数:不可修改类成员变量(指针例外,可以修改指针指向的内容)、可被不包含const的重载
-
const类对象:只可调用const成员函数
sizeof和strlen区别
-
sizeof为操作符,可接变量或数据类型,编译已有结果,对数组操作不退化(只算栈大小、因此static不计算在内)
-
strlen为库函数,只可接字符串,运行期才有结果,对数组操作为指针大小
malloc和new的区别
-
malloc为函数,仅分配内存,不调用构造函数、void*
-
new为操作符,可重载,调用构造函数、类型*
对齐原则(结构体或类空间分布)
-
结构体变量的首地址能够被其
-
结构体每个成员相对于结构体首地址的偏移量都是成员
-
结构体的总大小为结构体
-
有效对齐值=
-
使用伪指令#pragma pack(n):C编译器将按照n个字节进行对齐;
-
使用伪指令#pragma pack():取消自定义的字节对齐方式;
-
另外,还有GCC的特有语法
c/c++ struct区别
-
c语言struct无继承、无函数、无访问修饰符
-
c++都有
extern ”C“
-
c++支持c编译
-
c语言不支持函数重载
struct和class区别
-
默认继承权限不同,class-private,struct-public
-
默认访问权限不同,class-private,struct-public
-
struct不支持模板,为了兼容c代码
右值引用
c++ primer解释:
一个表达式是左值还是右值,取决于我们使用的是它的值还是它在内存中的位置(作为对象的身份)。也就是说一个表达式具体是左值还是右值,要根据实际在语句中的含义来确定。例如
-
在大多数情况下,需要右值的地方可以用左值来
-
需要左值的地方,一定不能用右值来替代
-
左值存放在对象中,
-
右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,
不论是左值引用还是右值引用,都有
-
-
它是右值。
-
从函数返回值中得到临时对象
-
销毁
-
将
-
销毁
完整文章:https://liam.page/2016/12/11/rvalue-reference-in-Cpp/
与右值引用极度相似,但是必须要满足两个条件:
-
必须格式是T&&
-
必须有类型推到,一般是auto&&、模板参数两种情况(模板不全是,需分析)
-
声明为auto&&的变量就是universal references
-
class vector<Widget, allocator<Widget>>{
public:
void push_back(Widget&& x); //rvalue reference
...
};
template<class T,class Allocator=allocator<T>>
class vector{
public:
template <class... Args>
void emplace_back(Args&&... args); // 通用引用
...
};
-
完整文章:https://www.kancloud.cn/kangdandan/book/169997
四种转换类型
-
static_cast:强制类型转换,无限制,类似于c中()强制转换
-
const_cast:转换表达式的const属性
-
dynamic_cast:运行时识别
-
reinterpret_cast:把二进制重新解释,如字符和int转化,int和float转换
C++智能指针
RAII:资源获取即初始化技术,在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间
-
Unique_ptr:独占指针,移动构造&赋值
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args&& ...args ) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
-
shared_ptr:引用计数,为0则释放资源:make_shared
-
Weak_ptr:配合shared_ptr,解决循环引用的问题
自己实现unique_ptr:
template<typename T>
class MyUniquePtr
{
public:
explicit MyUniquePtr(T* ptr = nullptr) // explict显示声明构造函数
:mPtr(ptr)
{}
~MyUniquePtr()
{
if(mPtr)
delete mPtr;
}
// 移动构造函数,移动操作符
MyUniquePtr(MyUniquePtr &&p) noexcept;
MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;
// 去除掉拷贝构造函数、拷贝赋值
MyUniquePtr(const MyUniquePtr &p) = delete;
MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
// 提供的对外操作
T& operator*() const noexcept {return *mPtr;}
T* operator->() const noexcept {return mPtr;}
T* get() const noexcept {return mPtr;}
// 用来做if(p)这种判断,默认是true
explicit operator bool() const noexcept{return mPtr;}
// 按需分配、非重要功能
void reset(T* q = nullptr) noexcept
{
if(q != mPtr){
if(mPtr)
delete mPtr;
mPtr = q;
}
}
T* release() noexcept
{
T* res = mPtr;
mPtr = nullptr;
return res;
}
void swap(MyUniquePtr &p) noexcept
{
using std::swap;
swap(mPtr, p.mPtr);
}
private:
T* mPtr;
};
template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
if(*this != p)
{
if(mPtr)
delete mPtr;
mPtr = p.mPtr;
p.mPtr = NULL;
}
return *this;
}
template<typename T>
MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
{
p.mPtr == NULL;
}
以下情形鼓励使用noexcept:为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。
移动构造函数(move constructor)
移动分配函数(move assignment)
析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。
手撕shared_ptr、weak_ptr实现:
https://blog.csdn.net/dong_beijing/article/details/79504591
设计一个不能被继承的类
核心点:
vector底层原理
-
reserve预留空间、不改变大小,如何释放空间?(push_back会增大空间,只有析构会释放)
-
resize改变大小、构造对象
友元
友元函数
-
-
using namespace std;
class CCar; //提前声明CCar类,以便后面的CDriver类使用
class CDriver
{
public:
void ModifyCar(CCar* pCar); //改装汽车
};
class CCar
{
private:
int price;
friend int MostExpensiveCar(CCar cars[], int total); //声明友元
friend void CDriver::ModifyCar(CCar* pCar); //声明友元
};
void CDriver::ModifyCar(CCar* pCar)
{
pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total) //求最贵气车的价格
{
int tmpMax = -1;
for (int i = 0; i<total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main()
{
return 0;
}
友元类
-
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的
-
class CCar
{
private:
int price;
friend class CDriver; //声明 CDriver 为友元类
};
class CDriver
{
public:
CCar myCar;
void ModifyCar() //改装汽车
{
myCar.price += 1000; //因CDriver是CCar的友元类,故此处可以访问其私有成员
}
};
int main()
{
return 0;
}
STL 内存池原理
理念:操作系统内存分配很耗时, 一次分配, 多次使用, 自然而然提高了效率
数据结构:一个指针数组(大小为16,按照8-128字节分)、多个自由链表
private:
static const int Align = 8;
static const int MaxBytes = 128;
static const int NumberOfFreeLists = MaxBytes / Align;
static const int NumberOfAddedNodesForEachTime = 20;
union node {
union node *next;
char client[1];
};
static obj *freeLists[NumberOfFreeLists];`
使用union的理由:
基本思路:
-
size>128,直接malloc分配,否则找到大小合适的链表,分出一个节点
-
size>128,直接free释放,否则将内存放置在大小合适的链表中
流程:
-
使用allocate向内存池请求size大小的内存空间, 如果需要请求的内存大小大于128bytes, 直接使用malloc.
-
如果需要的内存大小小于128bytes, allocate根据size找到
a. 如果链表不为空, 返回第一个node, 链表头改为第二个node.
b. 如果链表为空, 使用blockAlloc请求分配node.
x. 如果内存池中有大于一个node的空间, 分配竟可能多的node(但是最多20个), 将一个node返回, 其他的node添加到链表中.
y. 如果内存池只有一个node的空间, 直接返回给用户.
z. 若果如果连一个node都没有, 再次向操作系统请求分配内存.
①分配成功, 再次进行b过程
②分配失败, 循环各个自由链表, 寻找空间
I. 找到空间, 再次进行过程b
II. 找不到空间, 抛出异常(代码中并未给出, 只是给出了注释)
-
用户调用deallocate释放内存空间, 如果要求释放的内存空间大于128bytes, 直接调用free.
-
否则按照其大小找到合适的自由链表, 并将其插入.
详解文档:https://www.cnblogs.com/nzhl/p/5753728.html
select、poll、epoll区别
https://imageslr.com/2020/02/27/select-poll-epoll.html
select
-
监控的socket最大为FD_SIZE,大小受限制
-
只知道有数据返回,却不知道是哪个socket、只能无差别轮询、复杂度O(N)
-
socket数组频繁在内核空间与用户空间切换(
-
原理是按位与来查看状态、数组结构
poll
-
与select基本一致,只是换成了链表结构,去除了大小限制
epoll
-
水平触发、边缘触发(只通知一次,效率高,而且可以去掉一些不关心的文件描述符)为什么
-
无大小限制,ready list为双链表结构O(1),监控的描述符为红黑树结构,为O(N),还有一个存储进程的结构(用来通知进程)
-
明确知道哪些socket有返回,无需轮询;也不需要每次都重新把描述符加入数组
-
mmap映射到同一块内存区,减少copy