日常问题记录
# 2021-12-27
学习网址:https://subingwen.cn/link/
- 内存如何分配? 需要看C++内存管理课程
每个程序启动后都有自己的堆区,栈区,常量区,全局静态区吗? ——是 这部分内容涉及操作系统原理和程序的运行机制 语法扎实-UNIX环境高级编程-操作系统原理-LINUX内核涉及与实现-深入理解LINUX内核-程序员的自我修养 编译器既然能在函数调用后自动释放局部变量,为什么不能在每次调用完函数后自动添加一句free()代码来把本函数所有堆内存都释放呢?这样程序员不就可以不用亲自管理堆内存了吗? 栈区的内存其实更准确的应该叫丢弃,函数返回时,只是一个简单的对SP寄存器的值的一个改变即形成了栈的恢复动作,栈一恢复了,那之前那段栈内存上的数据肯定找不到了,所以会自动丢弃局部变量。 对堆内存的操作实际是动态内存操作,就涉及到内存管理了,其实现在的操作系统内核对用户空间的进程这块的创建与注销都加入了内存保护,你在用户空间的程序中释不释放堆内存,系统都会在这个程序结束时做内存回收动作,但这仅限于用户空间的程序,如果是内核编程的话,就一定要严格释放掉动态分配的内存,否则造成系统内存泄漏,内存泄漏的后果就是可用内存被你人为的弄成了不可用内存,到最后导至系统无动态内存可分配,就无法加载程序。
Linux 的虚拟内存管理有几个关键概念: 1、每个进程都有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址; 2、虚拟地址可通过每个进程上的页表(在每个进程的内核虚拟地址空间)与物理地址进行映射,获得真正物理地址; 3、如果虚拟地址对应物理地址不在物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。
32位的系统的地址空间就是我们的2^32字节(4GB),而64位的地址空间大小就是2^64个字节。这也就解释了在我们32位的操作系统,为什么最大只能支持4GB的有效内存。1G=1024MB,1MB=1024KB, 1MB=1024字节
虚拟地址与物理地址相关解释:https://www.jianshu.com/p/b6356e0ec63c
Linux内存分配:https://blog.csdn.net/gfgdsg/article/details/42709943
深入理解new和delete:https://blog.csdn.net/zhaxun/article/details/120139067?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164057769416780269895537%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164057769416780269895537&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-120139067.first_rank_v2_pc_rank_v29&utm_term=zhaxun&spm=1018.2226.3001.4187
https://www.zhihu.com/question/290504400
https://zhuanlan.zhihu.com/p/451469447
- 前置++与后置++
一般而言,后置的++运算符先将其值返回,然后其值增1;而前置的++运算符,则是先将值增1,再返回其值。对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符,一般推荐使用前置自增运算,因为前置自增(++i)通常比后置自增(i++)效率更高,由于后置自增运算需要把原来变量的值复制到一个临时存储空间中,等运算结束后返回这个临时变量
a++的实现为:
int temp;
temp = a;
a = a + 1;
return temp;
由于a++返回的值是编译器临时分配的临时变量temp,而temp并不是程序中定义的可寻址变量,因此不能作为左值。 因此,(a++) += a是不合法的。
# 2021-12-28
- 可变参数
VA_ARGS 是一个可变参数的宏
C++11要求,当字符串跟变量连接的时候,必须增加一个空格才行。因此解决方案有2个:
(1) Makefile文件明确告知编译方式采用C++98:CFLAGS += -std=c++98
(2)采用控制宏方式:
#if __cplusplus < 201103L
#define DEBUG(format, ...) \
fprintf(stderr, "[DEBUG][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);//__VA_ARGS__ === a,b
#define ERROR(format, ...)\
fprintf(stderr, "[ERROR][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#define LOG(format, ...)\
fprintf(stderr, "[LOG][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#else
#define DEBUG(format, ...) \
fprintf(stderr, "[DEBUG][%s:%d][%s][%s]" format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#define ERROR(format, ...)\
fprintf(stderr, "[ERROR][%s:%d][%s][%s]" format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#define LOG(format, ...)\
fprintf(stderr, "[LOG][%s:%d][%s][%s]" format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#endif
C语言printf家族函数的成员:
#include <stdio.h>
int printf(const char *format, ...); //输出到标准输出
int fprintf(FILE *stream, const char *format, ...); //输出到文件
int sprintf(char *str, const char *format, ...); //输出到字符串str中
int snprintf(char *str, size_t size, const char *format, ...);//按size大小输出到字符串str中
//以下函数功能与上面的一一对应相同,只是在函数调用时,把上面的...对应的一个个变量用va_list调用所替代。在函数调用前ap要通过va_start()宏来动态获取。
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
//使用示例
void stdio_printf(const char* fmt, ...)
{
uint16_t len, i;
va_list ap;
char buffer[256];
va_start(ap, fmt);
len = vsnprintf(buffer, sizeof(buffer), (const char*)fmt, ap);
printf("[%s]%s", "debug", buffer);
va_end(ap);
}
- 大端和小端
大端小端,理解成高尾端和低尾端,—— 尾端放在高地址还是低地址
"11223344" 尾端 44
高尾端:11223344
低尾端:44332211
/*
* 功能描述: 用于将2字节数据变为UINT16, BIG endian
* 参数说明: pInput, 为输入
* 返回值: 变换后的UINT16值
*/
UINT16 ShortFromChar(const UINT8 *pInput)
{
UINT16 Tempshort;
Tempshort = ( *(pInput) );
Tempshort = ( Tempshort<<8 ) + ( *(pInput+1) );
return Tempshort;
}
/*
* 功能描述: 用于将UINT8变为UINT16, LITTLE endian
* 参数说明: pInput, 为输入
* 返回值: 变换后的UINT16值
*/
UINT16 ShortFromCharLE(const UINT8 *pInput)
{
UINT16 Tempshort;
Tempshort = ( *(pInput+1) );
Tempshort = ( Tempshort<<8 ) + ( *(pInput) );
return Tempshort;
}
- 友元
留着和运算符重载一起 https://max.book118.com/html/2016/1116/63351868.shtm
3.1 友元的交叉引用
//2-line.h
#ifndef _LINE_H_
#define _LINE_H_
class Point;
class Line{
pulic:
float getDistance(const Point& a, const Point& b);
};
#endif
//2-point.h
#ifndef _POINT_H_
#define _POINT_H_
#include "2-line.h"
class Point{
friend float Line::getDistance(const Point& a, const Point& b);
public:
Point(float x, float y);
private:
static int s_count;
float _x;
float _y;
};
#endif
# 2021-12-30
- 运算符重载(cout,new,delete,等)
- 右值引用,移动,完美转发
什么使用使用前置声明,什么时候使用include
# 2022-1-4
- 构造函数为什么不能是虚函数? C++的类中,构造函数用于初始化对象及相关操作,构造函数是不能声明为虚函数的,因为在执行构造函数前对象尚未完成创建,虚函数表还不存在。 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储成员函数指针的数据结构。 虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,当存在虚函数时,每个对象都有一个指向虚函数的指针(vptr指针)。在实现多态的过程中,父类和派生类都有vptr指针。 vptr的初始化:当对象在创建时,由编译器对vptr指针进行初始化。在定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。 如果构造函数时虚函数,那么调用构造函数就需要去找vptr,而此时vptr还没有初始化。 因此,构造函数不可以是虚函数。
- C++基类的析构函数为何要声明为虚函数? 析构函数则用于销毁对象完成时相应的资源释放工作,析构函数可以被声明为虚函数。在继承层次中,基类的析构函数一般建议声明为虚函数。 将基类的析构函数声明为虚函数之后,派生类的析构函数也自动成为虚析构函数,在主函数中基类指针pBase指向的是派生类对象,当delete释放pBase指针所指向的存储空间时, 首先执行派生类的析构函数(derived destructor); 然后执行基类的析构函数(base destructor)。 综上所述,将基类的析构函数设为虚函数,可以保证派生类被正确地释放。 调用子类析构函数后,由于是父类指针,被delete时,会调用父类析构函数。
- 虚函数指针和虚函数表 C++虚函数剖析 每个包含了虚函数的类都包含一个虚表。一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。 虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的累的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*_vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
涉及到c++对象模型 RTTI (https://zhuanlan.zhihu.com/p/150579874) 运行时类型识别 在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。 C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。 C++通过以下的两个操作提供RTTI: (1)typeid运算符,该运算符返回其表达式或类型名的实际类型。 (2)dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。 在虚函数表的前面,其实还有一个指向type_info对象的指针,以帮助程序在运行时获取对象的类型信息。那么什么是type_info对象呢?
class type_info
{
public:
virtual ~type_info();
bool operator==(const type_info&)const;
bool operator!=(const type_info&)const;
bool before(const type_info&)const;
const char* name()const;
private:
type_info(const type_info&);
type_info& operator=(const type_info&);
};
//const char* name() const:返回一个能表示类型名称的字符串。但是C++标准并没有规定这个字符串是什么形式的
//bool before (const type_info& rhs) const:判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。
//bool operator== (const type_info& rhs) const:重载运算符“==”,判断两个类型是否相同,rhs 参数是一个 type_info 对象的引用。
//bool operator!= (const type_info& rhs) const:重载运算符“!=”,判断两个类型是否不同,rhs 参数是一个 type_info 对象的引用。
为了深刻理解RTTI和type_info机制,我们来写一个例子:
#include <iostream>
using namespace std;
class Base{
public:
virtual void func();
protected:
int m_a;
int m_b;
};
void Base::func(){ cout<<"Base"<<endl; }
class Derived: public Base{
public:
void func();
private:
int m_c;
};
void Derived::func(){ cout<<"Derived"<<endl; }
int main(){
Base *p;
int n;
cin>>n;
if(n <= 100){
p = new Base();
}else{
p = new Derived();
}
cout<<typeid(*p).name()<<endl;
return 0;
}
- 虚基类与虚基表
学习网站: https://coolshell.cn/articles/12165.html , http://blog.csdn.net/haoel , https://blog.csdn.net/lvruyi/article/details/76101047
以上两部分主要通过C++对象模型一书进行学习。