namespace
相当于在名字前面套了一层,就可以在不同namespace起相同名字。分为定义和使用2种行为。
定义
namespace XXX{
int a=14123;
}
整个脚本使用。也可以写多个using命名空间,这些命名空间的对象都不用加 命名空间::
using namespace XXX;
int main(){
a=234782;
}
局部使用
int main(){
XXX::a=234782;
}
vector脚本文件的开头有namespace std吗,我怎么没看到?
在C++标准库中,vector确实定义在std命名空间内,但通常不会直接在脚本文件开头显式出现namespace std的声明。以下是具体解析:
1. 标准库的实现方式
标准库的实现文件(如<vector>头文件)内部会包含类似namespace std { template... }的定义,但这是库开发者实现的细节,用户无需直接看到。用户只需通过#include <vector>引入头文件即可使用std::vector。
2. 用户代码中的常见写法
用户代码中通常通过以下两种方式使用vector:
显式限定命名空间:直接使用std::vector<int> vec;,此时无需在文件开头声明namespace std。
using指令简化:若在文件开头添加using namespace std;,后续可直接使用vector,但这种方式可能引发命名冲突(尤其在大型项目中)。
3. 为何看不到namespace std声明
标准库的头文件(如<vector>)已将所有内容封装在std命名空间内,用户仅需包含头文件即可访问相关功能,无需手动声明命名空间。
若用户自定义命名空间,才需要显式编写namespace xxx {...},而标准库的命名空间由编译器自动处理。
总结
std命名空间的声明隐藏在标准库实现文件中,用户通过包含头文件间接使用。若需明确代码归属,推荐使用std::vector而非全局using namespace std。
2的n次方
C++里^表示按位异或,写2的n次方是
1<<n;
就是把最低位1左移n位。
i++和++i
i++=3;是什么意思
这样的语句是非法的,无法通过编译。
这是因为 i++ 是一个右值表达式,它返回的是 i 自增前的临时副本,而不是 i 本身。右值是一个临时的、没有持久内存地址的值,它只能出现在赋值运算符的右边,不能被赋值。
与之相对,++i 是一个左值表达式,它返回的是自增后的 i 变量本身,因此可以被赋值。例如,++i = 3; 在语法上是合法的(尽管在实际编程中通常不这样使用),其效果相当于先执行 i = i + 1,然后再执行 i = 3
sizeof()
在C++和C#的区别
在C# sizeof()只能输入值类型名,C++ sizeof()可以输入类型名或变量,输入
- 变量引用时返回被引用对象本身所占的内存空间大小。
- 指针时返回指针的字节数,4或8;
- 数组名字返回数组内容占的字节数(数组作为函数参数传入时退化为指针,走情况2,所以要另外传一个int长度);
指针
类型* 是该类型指针;
&变量 是取地址;
*指针 是取值;
指针在32位系统4字节,64位系统8字节,与指针类型无关。
int a=13;
int*p=&a;
cout<<*p;
指针运算
指针++就是内存移动它指向类型的字节数,指向下一个它指向的类型。
const修饰指针
常量指针
int a=10;
int b=412;
const int*p=&a;
*p=213;//不行
a=341;//可以
p=&b;//可以
*p=128;//不行
b=1344;//可以
指针的指向可以修改,指针指向的值不能修改,但是p指向的变量可以通过非指针方式修改。
指针常量
int a=10;
int f=432;
int* const p=&a;
p=&f;//不行
*p=4231;//可以
指针的指向不能修改,指针指向的值可以修改。
常量指针常量
int a=10;
int f=432;
const int* const p=&a;
p=&f;//不行
*p=4231;//不行
&的含义
- 声明变量,放在类型后面,表示变量的引用(相对于变量别名);
- 放在变量前面,取指针;
- 放在函数参数类型后面,引用传递,直接在原变量身上操作,不拷贝数据;
new、delete
在堆内存分配、释放内存。new返回类型的指针。释放数组时用delete[]。
int*p=new int(123);
delete p;
int* arr=new int[10];
delete[] arr;
引用
声明时必须初始化。初始化后不能改。
int a=10;
int &b=a;
引用参数
void MySwap(int &a, int &b){
}
引用的本质:指针常量。
引用做输入参数
void ShowValue(int &val){
val=1000;
cout<<val;
}
int main(){
int a=13;
ShowValue(a);
}
string
string除了字符串本身还有几个字节
C++中string对象除了存储字符串内容外,还会占用额外的内存空间用于管理数据,具体大小取决于编译器和实现方式:
Visual Studio环境下
string对象固定占用28字节,包含:
16字节的联合体(用于小字符串优化,长度<16时用内部数组,≥16时用堆指针)
4字节存储字符串长度
4字节存储堆空间总容量
4字节的其他管理指针
G++环境下
采用写时拷贝技术,string对象仅占4字节,内部仅包含一个指向堆空间的指针,该指针指向的结构包含长度、容量等字段
基础结构说明
string本质是basic_string<char>的别名,其内存占用与实现相关,通常包含:
字符串数据(可能存储在栈或堆)
长度标记
容量标记
分配器信息
注意:使用sizeof(string)得到的是对象本身大小,而非字符串内容占用的内存。例如对"hello"字符串,string对象可能占28字节(VS),而内容实际占用6字节(含'\0')。
函数
inline内联函数
执行函数时不开辟新的调用栈(也就没有参数值拷贝那些),编译器直接把函数代码拷贝到调用处。
inline是建议编译器内联,如果函数代码很长,编译器会退化成普通函数,仍然使用栈帧。
纯虚函数
virtual void XXX()=0;
结构体
成员不需要写public、private(和C#不同),默认public。
#include<iostream>
#include<string>
struct Student{
int ID;
string name;
int age;
}s4;//顺便创建结构体变量
int main(){
Student s1={4,"林小满",13};
return 0;
}
结构体能继承吗?
能。唯一的区别在于默认的成员访问权限和继承方式(默认public)。
结构体含有字符串,那么每个结构体实例的字节数相同吗???
在C++中,结构体包含字符串成员时,其实例的字节数是否相同取决于字符串的具体实现方式。以下是关键分析:
1. 使用 std::string 的情况
固定大小:std::string 对象本身在栈上占用固定大小的内存(通常为24或40字节,取决于系统和实现),但其实际字符串内容存储在堆上。因此,结构体的 sizeof 结果固定,与字符串内容长度无关。
struct WithStdString {
std::string str; // 固定大小(如24字节),实际字符串在堆中
int num;
};
所有实例的 sizeof(WithStdString) 相同,但堆内存占用随字符串内容变化。
原因:std::string 内部通常包含指针、容量和长度等元数据,这些元数据的大小是固定的。
2. 使用字符数组(如 char[])的情况
固定大小:若字符串为固定长度的字符数组,结构体大小固定,由数组声明长度决定。
struct WithCharArray {
char str[100]; // 固定占用100字节
int num;
};
无论实际字符串多短,sizeof(WithCharArray) 始终为104字节(假设4字节对齐)。
3. 动态分配字符指针(如 char*)的情况
固定指针大小:指针本身占用固定大小(通常4或8字节),但指向的字符串内容在堆上,结构体 sizeof 结果不包含堆内存。
struct WithCharPointer {
char* str; // 指针固定大小(如8字节)
int num;
};
实例的 sizeof(WithCharPointer) 相同,但实际内存占用取决于字符串长度。
4. 字节对齐的影响
结构体大小还需考虑字节对齐规则,成员排列顺序可能影响填充字节数,但同一结构体类型的对齐规则对所有实例一致。
例如,若 std::string 和 int 混用,编译器可能插入填充字节以满足对齐要求,但所有实例的填充方式相同。
总结
字符串类型 sizeof 结果 实际内存占用
std::string 固定 可变(堆内存)
char[N] 固定 固定(栈内存)
char* 固定 可变(堆内存)
结论:结构体实例的 sizeof 结果相同(因元数据或数组长度固定),但实际内存消耗可能因字符串内容而异。
类和对象
和C#的区别:
- class前面不用写public也能访问;
- 花括号结束后有分号;
- 公开成员可以只写一个public:多个成员;
- 实例化不用new创建在栈内存,用new创建在堆内存(对int等类型也一样。C++没有值类型引用类型的区分,有直接创建在栈和new创建在堆的区分);
- 赋值、参数传入、返回传出如果不加&则是值传递;
- 静态成员C#用类访问,C++可以用类::成员访问或实例.成员访问;
class Person{
int age;
public:
Person(){}
Person(int age){
this->age=age;
}
Person(int a,int b):ID(a),age(b){
}//对象初始化列表
Person& AddAge(Person&p){//&传入引用参数防止把参数复制一份。返回&如果没有会把自己复制一份
this->age+=p.age;
return *this;
}
static int eyesNum;//静态成员,类内声明,类外初始化
};
int Person::eyesNum=2;
int main(){
Person p0;
Person p1(31);
Person p2=Person(10);//显式
Person(13);//匿名对象,当前行执行完立即回收
Persons p4=10;//隐式转换,相当于p4(10);
}
拷贝构造函数:传入一个同类对象。
class Person{
public:
Person(const Person &p){
}
};
int main(){
Person p;
Person p2(p);
Person p3=Person(p);
Person p4=p3;//隐式转换法
}
无任何内容的空对象占1个字节,有内容后占位字节取消,字节数取决于字段。
this指针
指向正在调用非静态函数的对象,this->访问成员字段,返回本对象为return *this;
空指针调用函数如果里面没有this,就可以调用。
class Person{
public:
void ShowPerson(){
std::cout<<"哈哈哈哈哈哈";
}
void Doit() const//常函数,不允许修改字段,加mutable可以修改
{
//this->age=988;
this->HP=3124;
}
int age;
mutable int HP;
};
int main(
Person* p=NULL;
p->ShowPerson();//空指针调用函数如果里面没有this,就可以调用。
const Person p2;//常对象,禁止修改字段,但是加mutable的字段可以修改
//p.age=1423;//常对象只能调用常函数
return 0;
}
private的可访问范围
可以在一个A类对象a1的函数内访问另一个A类对象a2的private成员。private意思是本类的任意对象内可访问本成员,而不限于本对象!!!!!!!!!!!!!!!!!
访问父类同名成员
son.Base::age;
struct、class区别
默认访问权限不同,struct默认public,class默认private。
友元
友元全局函数
#include<string>
class House{
friend void GoodGay(House* house);//此函数可以访问私有成员
public:
string hall;
private:
string bedroom;
};
void GoodGay(House* house){
std::cout<<"访问:"<<house->hall;
}
int main(){
友元类
#include<iostream>
#include<string>
class House;
class Pal{
public:
void Visit();
House* house;
Pal(){
house=new House;
}
};
class House{
friend class Pal;
public:
string hall;
House();
private:
bedroom;
};
House::House(){//函数类外实现
hall="客厅";
bedroom="卧室";
}
void Pal::Visit(){
std::cout<<"参观!!!!"<<house->bedroom;
}
int main(){
Pal p1;
p1.Visit();
return 0;
}
友元成员函数
#include<iostream>
#include<string>
class House;
class Pal{
public:
Pal(){
house=new House;
}
void Visit();
House* house;
};
void Pal::Visit(){
std::cout<<"让我看看!!!!!"<<house->bedroom;
}
class House{
friend void Pal::Visit();
public:
string hall;
private:
string bedroom;
}
House::House(){
hall="客厅";
bedroom="卧室";
}
int main(){
Pal pal1;
pal.Visit();
return 0;
}
运算符重载
#include<iostream>
class Man{
public: int age;
Man operator+(Man &man){
Man temp;
temp.age=this->age+man.age;
return temp;
}
};
Man operator+(Man &m1,Man &m2){
Man temp;
temp.age=m1.age+m2.age;
return temp;
}
std::ostream& operator<<(std::ostream &cout, Man&m) {
cout << "年龄是:" << m.age << std::endl;
return cout;
}
int main(){
Man man1;
man1.age=12;
Man man2;
man2.age=41;
Man man3=man1+man2;
std::cout << man3.age<< std::endl;
std::cout << (man1 + 52).age<< std::endl;
std::cout << man3<<std::endl;
return 0;
}
自增++
#include <iostream>
class MyInt {
public: int value;
MyInt operator+(MyInt &myInt) {
MyInt temp;
temp.value = this->value + myInt.value;
return temp;
}
MyInt& operator++() {//返回引用,为了多次自增时对同一个对象操作!
value++;
return *this;
}
MyInt operator++(int) {//实际上复制了一个新对象返回
MyInt temp = *this;//复制一份,生命周期仅限函数内
value++;
return temp;//没返回引用,又复制一份
}
};
MyInt operator+(MyInt &m1, int num) {
MyInt temp;
temp.value = m1.value + num;
return temp;
}
std::ostream& operator<<(std::ostream &cout,MyInt m) {
cout<< m.value;
return cout;
}
int main() {
MyInt m1;
m1.value = 12;
MyInt m2;
m2.value = 41;
MyInt m3 = m1 + m2;
std::cout << m3.value<< std::endl;
std::cout << (m1 + 52).value<< std::endl;
std::cout << ++m3<<std::endl;
std::cout << m3 << std::endl;
std::cout << m2++ << std::endl;
std::cout << m2 << std::endl;
return 0;
}
cout cin
分别是ostream、istream对象,它们重载了<<、>>运算符,把能打印、接收的类型作为第二个参数,还返回ostream、istream,实现了链式调用。
while(cin>>)
void Hahaha() {
string f;
while (cin >> f) {}
}
怎么停止:回车>ctrl z>回车
cin>>进制
cin>>hex
cin>>dec
cin>>oct
cin和getline
- cin遇到空格就停止,getline()读取整行。
- 当
cin >>在输入开头遇到换行符时,会忽略并清除该换行符,然后继续等待有效数据的输入。getline()开头遇见换行符则直接返回!cin后需要cin.ignore()清除换行符,如果后面要getline()
读取有空格的输入
string i;
getline(cin, i);
继承
public继承:不改变父类字段的访问权限,protect继承:令父类字段访问权限不高于protected,private继承:令父类字段全部为private。
访问父类的同名成员x.Base::a;
菱形继承
孙类会有2份爷类的字段,必须通过孙类对象.父类::字段访问。
class Animal{
public:
int age;
};
class Dog:public Animal{
};
class Cat:public Animal{
};
class MyPet:public Dog, public Cat{
};
void Test{
MyPet pet;
pet.Dog::age=13;
pet.Cat::age=41;
}
虚继承
子类有一个vbptr虚基类指针,vbptr每个子类对象有一个,指向虚基类表(vbtable)。
vbtable每个子类有一个,存在常量区。
虚继承子类访问字段时通过vbptr找到vbtable,查到这个字段在对象内存的偏移量,访问。
class Animal{
public:
int age;
};
class Dog:virtual public Animal{
};
class Cat:virtual public Animal{
};
class MyPet:public Dog, public Cat{
};
void Test{
MyPet pet;
pet.age=13;
}

基类只有一个int字段,子类虚继承基类,一个子类对象有几个字节?
32位系统,int+vbptr=8字节,64位系统不考虑对齐,16字节。32位系统中,vbptr在开头,int偏移量为4字节。
子类虚继承父类,孙类普通继承子类,孙类有没有vbptr?
孙类是否包含vbptr需结合具体编译器和继承结构判断,通常以复用子类vbptr为主,但特定条件下可能新增。
子类虚继承父类,孙类普通继承子类,孙类有没有vbtable?
孙类通常不需要独立vbtable,但具体行为由编译器决定。
如果孙类继承了两个虚继承爷类的父类,那孙类的字段偏移量比父类的要多出vbptr的字节数,不就不能用父类的vbtable了吗?
在多重虚继承场景下,孙类的内存布局和虚基表(vbtable)使用机制如下:
1. 虚基表指针(vbptr)的独立性
当孙类继承两个虚继承自同一爷类的父类时,每个父类会维护独立的vbptr指向各自的vbtable。孙类会继承这两个vbptr,但不会直接复用父类的vbtable,而是生成新的虚基表来统一管理虚基类偏移量。
例如,若父类B和C均虚继承爷类A,孙类D继承B和C时,D的内存布局会包含B和C的vbptr,但D的虚基表会重新计算A的偏移量,而非直接使用B或C的偏移量。
2. 偏移量计算的动态性
虚基表的偏移量是相对于当前对象起始地址的。孙类对象中,父类的vbptr可能因新增字段或对齐填充而位置变化,但通过以下机制保证正确访问:
孙类的虚基表会记录虚基类(爷类A)相对于孙类对象起始地址的偏移量,而非父类对象的偏移量。
访问虚基类成员时,编译器通过孙类的vbptr找到虚基表,再动态计算最终地址,与父类的vbptr无关。
3. 编译器优化与内存布局
GCC/Clang:可能合并冗余vbptr,但虚基表仍独立生成,确保偏移量正确。
MSVC:为每个虚继承链生成独立vbptr,孙类通过多个虚基表分别定位虚基类。
总结
孙类不会直接复用父类的vbtable,而是通过新增的虚基表统一管理偏移量。虚继承的核心是共享虚基类实例,而非共享虚基表,因此父类的vbtable偏移量不适用于孙类对象。
理解虚继承解决菱形继承
虚继承时改成由地址偏移量访问字段,两个虚继承的父类各有vbptr+int age字段(8字节),孙类有2个vbprt和一个int age字段(12字节)。2个vbptr指向2个vbtable,分别是在孙类对象内存中int age相对于每个父类起始点的偏移,相对于第一个父类(也就是孙类起始点)偏移8,相对于第二个父类偏移4.
虚函数
父类装子类,子类重写方法,执行的还是父类方法,需要父类写virtual。
有虚函数的对象有一个虚函数表指针。
纯虚函数:父类不需要实现,强制子类实现。
virtual XXX()=0;
虚函数表
虚函数表本质上是一个函数指针数组,每个元素存储类中虚函数的内存地址。当类包含虚函数时,编译器会隐式生成该表,并按声明顺序排列虚函数指针。
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
Base的vtable:
[0] -> Base::func1()
[1] -> Base::func2()
虚表指针(vptr):每个对象实例头部隐含一个指针成员,指向所属类的虚函数表。该指针在对象构造时由编译器自动初始化
子类的虚函数表:复制父类的,然后把重写的替换。
class Derived : public Base {
public:
void func1() override {}
virtual void func3() {}
};
Derived的vtable:
[0] -> Derived::func1() // 覆盖Base版本
[1] -> Base::func2() // 保留基类实现
[2] -> Derived::func3() // 新增虚函数
总结
- 虚函数表是一个类有一个,存在常量区;
- 虚表指针是一个对象有一个;
- 子类未重写方法则和父类共用虚函数表,重写了就复制一个然后修改;
虚析构、纯虚析构
问题:父类装子类时,析构时不会调用子类的析构函数,如果子类有堆区属性,则无法释放,把父类析构写成虚析构就可以。
virtual ~MyClass(){}
纯虚析构
virtual ~MyClass()=0;
};
MyClass::~MyClass(){};
有了虚析构为什么还需要纯虚析构?
纯虚析构函数的特殊需求
纯虚析构函数(virtual ~Base() = 0;)除了具备虚析构函数的多态销毁能力外,还有以下独特作用:
- 强制派生类实现析构逻辑:纯虚析构函数会将基类声明为抽象类,阻止其实例化,同时要求派生类必须提供析构函数的实现57。
- 仍需提供定义:与普通纯虚函数不同,纯虚析构函数必须在类外提供实现(如
Base::~Base() {}),否则派生类析构时无法调用基类析构函数,导致资源泄漏59。 -
典型使用场景对比
- 虚析构函数:适用于基类可能被继承且需要通过基类指针删除对象的场景13。
- 纯虚析构函数:适用于需要强制派生类实现析构逻辑,或明确禁止基类实例化的设计(如接口类)57。例如:
class AbstractInterface { public: virtual ~AbstractInterface() = 0; // 纯虚析构 }; AbstractInterface::~AbstractInterface() {} // 必须定义
-
为什么不能只用虚析构函数?
纯虚析构函数通过将基类变为抽象类,更清晰地表达设计意图(如“此类仅为接口”),而普通虚析构函数无法实现这一约束78。此外,纯虚析构函数强调派生类必须关注资源释放问题,适合严格的接口设计59。 -
总结:虚析构函数解决多态销毁问题,而纯虚析构函数在此基础上增加了对抽象类的支持和对派生类的强制要求,两者适用于不同的设计需求57。
VS 20xx的开发人员命令提示符
cd到脚本文件夹,
cl /d1 reportSingleClassLayout类名 脚本名
好像无法查看头文件里的类?


deepseek回答:
- 编译器可能因历史兼容性问题对
.h扩展名处理异常,可尝试改为.hpp或显式指定文件类型:bashCopy Code
其中cl /d1reportSingleClassLayoutMale /TP Male.h/TP强制将文件视为C++源文件3。
能解决。
模板
函数模板
template<typename T>//或template<class T>
void Func(T&a, T&b){}
在有同名非模板函数时强制调用模板函数:
Func<>(a,b);
类模板
template<typename Type1=string,typename Type2=int>//或template<class T>
class MyClass{
Type1 t1;
Type2 t2;
public:
MyClass(Type1 t1,Type2 t2){
this->t1=t1;
this->t2=t2;
}
int main(){
MyClass<string,int>mc("狷介",452);
return 0;
}
类模板对象做参数
template<class T1=string,class T2=int>
class Man{
T1 name;
T2 age;
public:
Man(T1 name,T2 age){
this->name=name;
this->age=age;
}
void ShowMan(){
std::cout<<name<<age<<endl;
}
};
void PrintMan1(Man<string,int>&m){
m.ShowMan();
}
template<class T1,class T2>
void PrintMan2(Man<T1,T2>&m){
m.ShowMan();
cout<<typeid(T1).name()<<endl;
}
template<class T>
void PrintMan3(T&m){
m.ShowMan();
}
int main(){
Man<string,int>m("大意啊",41);
PrintMan1(m);
PrintMan2(m);
PrintMan3(m);
return 0;
}
继承模板类
template<class T>
class Base{
T t;
};
//指定T类型
class Son:public Base<int>{
};
//子类也是模板
template<class T1,class T2>
class Son2:public Base<T1>{
T2 obj;
};
类外实现
template<class T1,class T2>
class Man{
T1 name;
T2 age;
public:
Man(T1 name,T2 age);
void ShowMan();
};
//类外实现
template<class T1,class T2>
Man<T1,T2>::Man(T1 name,T2 age){
this->name=name;
this->age=age;
}
template<class T1,class T2>
void Man<T1,T2>::ShowMan(){
std::cout<<name<<age<<endl;
}
类模板和友元
//提前让编译器知道Man类存在
template<class T1,class T2>
class Man;
//让编译器知道这个函数存在
template<class T1,class T2>
void PrintMan2(Man<T1,T2> m){
std::cout<<"姓名:"<<m.name<<"年龄:"<<age<<std::endl;
}
template<class T1,class T2>
class Man{
//全局函数,类内实现
friend void PrintMan(Man<T1,T2>m){
std::cout<<"姓名:"<<m.name<<"年龄:"<<age<<std::endl;
}
//全局函数,类外实现
//加空模板参数列表
//如果全局函数类外实现,需要让编译器提前知道这个函数的存在
friend void PrintMan2<>(Man<T1,T2> m);
string name;
int age;
};
链表
把一个数组变成链表的流程:
- 声明头节点指针,new一个节点;
- 如果数组长度>1,
ListNode* CreateList(int arr[],int num) {
ListNode* head = new ListNode(arr[0]);
if (num > 1) {
ListNode* prober = head;
for (int i = 1; i < sizeof(arr); i++) {
prober->next = new ListNode(arr[i]);
prober = prober->next;
}
}
return head;
}
STL
vector
#include <iostream>
#include <vector>
#include<algorithm>
void 测试矢量() {
std::vector<int>v;
v.push_back(24);
v.push_back(53);
for (std::vector<int>::iterator i = v.begin();
i != v.end(); i++) {
std::cout << *i << std::endl;
}
std::for_each(v.begin(), v.end(), Print);
}
push_back
push_back会发生值传递,如果是添加一个vector,会把它的所有元素复制一份。
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>>vv;
void Test(){
vector<int>v{31,41,64,13,8,31,85};
vv.push_back(v);
}
所以当函数内局部声明一个vector、引用传入vector,可以直接把它push_back,不用担心会被回收或改变原值。
unordered_map
底层实现
unordered_map底层实现原理
一、核心数据结构
基于哈希表实现,采用链地址法(哈希桶)解决冲突。主要包含动态数组(存储桶指针)和链表结构(存储键值对)。动态数组通常使用类似vector的连续存储结构,每个桶对应一个链表头指针,冲突元素以节点形式链接在对应桶后。
二、关键机制
哈希函数
默认使用std::hash模板计算键的哈希值,通过取模运算确定桶位置(hash(key) % bucket_count)。自定义类型需特化std::hash或提供哈希函数对象。
动态扩容
当负载因子(size()/bucket_count())超过阈值(默认1.0)时,自动扩容至约2倍桶数并重新哈希所有元素。扩容过程包括:
分配新桶数组
迁移原有元素
释放旧空间。
冲突处理
同一桶内的元素以单向链表存储(C++11前),部分实现可能采用红黑树优化高频冲突场景。查找时先定位桶,再遍历链表匹配键。
三、性能特性
操作 平均复杂度 最坏复杂度 条件
插入/删除 O(1) O(n) 哈希函数均匀分布时
查找 O(1) O(n) 所有元素冲突到同一桶时
四、与map的对比
特性 unordered_map map
底层结构 哈希表 红黑树
元素顺序 无序 按键排序
查找性能 平均O(1) O(log n)
内存占用 较高(需维护桶数组) 较低
适用场景 高频查找、无需排序 需有序遍历或范围查询
五、自定义实现示例
简化版哈希桶实现需包含以下组件:
动态桶数组(vector<list<pair<K,V>>>)
哈希函数与取模运算
负载因子检测与扩容逻辑。
典型操作流程:
cpp
Copy Code
// 插入示例
size_t bucket_idx = hash_func(key) % buckets.size();
buckets[bucket_idx].emplace_back(key, value);
if (load_factor() > max_load_factor) rehash();
该设计通过分离链地址法平衡冲突处理与内存效率,动态扩容机制确保操作性能稳定。
负载因子
不难看出,负载因子就是平均每个桶的元素个数。负载因子<=1时可能有碰撞,>1时一定有桶大于1个元素
map
#include<iostream>
#include<map>
void PrintMap(std::map<int,int>&m){
for(std::map<int,int,MyCmp>::iterator i=m.begin();i!=m.end();i++){
std::cout<<"键:"<<i->first<<"值:"<<i->second<<std::endl;
}
}
void Test(){
std::map<int,int,MyCmp>m1;
m1.insert(std::pair<int,int>(31,14));
m1.insert(make_pair(431,414));
m1[323] = 1411;
PrintMap(m1);
m1.erase(m1.begin());
m1.erase(31);
std::map<int,int>::iterator pos=m1.find(41);
if(pos!=m1.end()){
}
}
//仿函数
class MyCmp{
public:
bool operator()(int v1,int v2){
return v1>v2;
}
};
deque
#include<iostream>
#include<deque>
void PrintDeque(const std::deque<int>&d){
for(std::deque<int>::const_iterator i=d.begin();i!=d.end();i++){
std::cout<<*i<<" ";
}
std::cout<<std::endl;
}
void Test(){
std::deque<int>d1;
stf::deque<int>d2(d1);
d1.push_back(234);
d1.push_front(42);
d1.insert(d1.begin(),14);
d1.insert(d1.begin(),d2.begin(),d2.end());
PrintDeque(d1);
std::cout<<d1[0]<<" "<<d1.at(1)<<" "<<d1.front()<<" "<<d1.back();
std::deque<int>d2(d1.begin(),d1.end());
}
deque初始化时有几个内存块
根据STL实现细节,deque初始化时的内存块数量取决于以下机制:
默认初始块数
deque构造函数会预分配2个内存块(缓冲区),并将头尾迭代器指向中间位置以实现双向扩展能力。这种设计避免了立即分配过多内存,同时为后续操作预留空间。
动态调整规则
若未指定元素数量,仅分配最小块数(通常为2块)。
若构造时指定元素数量n,则通过公式num_nodes = n / chunk_size + 1计算所需块数,其中chunk_size由编译器决定(如512字节或系统页大小)。
中央控制器(map)的初始大小至少为8项,即使实际块数较少也会预留空间。
实现差异说明
GCC版本中,_Deque_base基类负责内存分配,通过_M_initialize_map函数动态计算块数并保证首尾预留空项,确保扩容时无需频繁重新分配map。
stack
#include <iostream>
#include <stack>
using namespace std;
void Test(){
stack<int> s1;
s1.push(2387);
s1.push(274);
s1.push(141);
cout<<s1.size()<<endl;
while(!s1.empty()){
cout<<s1.top()<<" ";
s1.pop();
}
}
stack的底层实现
C++ STL中stack的底层实现机制如下:
1. 容器适配器设计
stack是容器适配器而非独立容器,通过封装底层序列容器(默认deque)实现LIFO(后进先出)逻辑,仅暴露栈的标准接口(如push、pop、top)。
2. 默认底层容器:deque
选择原因:deque(双端队列)支持首尾O(1)时间复杂度的插入删除,且内存布局为分块连续结构,兼顾效率与空间利用率。
核心操作映射:
push() → deque::push_back()
pop() → deque::pop_back()
top() → deque::back()。
3. 可替换的底层容器
用户可通过模板参数指定其他容器(如vector或list),但需满足以下接口:
push_back()、pop_back()、back()、empty()、size()。
性能对比:
vector:尾部操作高效,但扩容需数据迁移。
list:稳定O(1)操作,但内存碎片化。
4. 实现特性
无迭代器:因LIFO特性限制,不提供遍历功能。
内联优化:成员函数调用底层容器接口时避免额外开销。
5. 应用场景
函数调用栈、表达式求值等需回溯的场景。
需快速访问栈顶且无需中间操作的场景。
总结:stack通过适配deque等容器,以最小封装代价实现了高效的LIFO数据结构,开发者可根据场景灵活选择底层容器。
LIFO不是完美适配vector吗,为什么用deque
在C++ STL中,尽管vector的尾部操作(push_back()/pop_back())与LIFO(后进先出)的stack需求高度匹配,但默认选择deque作为底层容器主要基于以下设计考量:
1. 避免扩容性能开销
vector在空间不足时需要重新分配连续内存并拷贝所有元素,扩容成本为O(n)。而deque的分段连续存储结构(多块固定大小内存)仅需追加新内存块,扩容代价更低。
典型场景:频繁入栈/出栈时,deque的内存增长更平滑,避免vector的突发性能波动。
2. 兼容其他容器适配器的统一性
queue需要高效的首尾操作(如pop_front()),而vector的头部删除是O(n)操作。为保持STL适配器的一致性,stack和queue统一采用支持双端操作的deque作为默认容器。
灵活性:用户仍可指定vector或list作为stack的底层容器,但需自行承担潜在性能代价。
3. 内存管理的友好性
deque的分页式内存分配对操作系统更友好,尤其处理大容量数据时:
避免vector单一大内存块导致的地址空间碎片化问题。
分段存储允许部分内存释放,而vector只能整体释放。
总结
deque在内存效率、操作稳定性和适配器统一性上综合优于vector,成为stack默认实现的平衡选择。若明确场景仅需尾部操作且预分配足够空间,手动指定vector可能更高效。
queue
#include <iostream>
#include <queue>
using namespace std;
void Test(){
queue<int>q1;
q1.push(529);
q1.push(474);
while(!q1.empty()){
cout<<q1.front()<<" ";
cout<<q1.back();
q1.pop();
}
}
queue的底层实现
C++ STL中queue的底层实现机制如下:
1. 容器适配器本质
queue是容器适配器而非独立容器,它通过封装底层容器(默认deque)实现FIFO逻辑,仅暴露队列的标准接口(如push、pop、front)。
2. 默认底层容器:deque
选择原因:deque(双端队列)支持首尾O(1)时间复杂度的插入删除,完美匹配队列的FIFO需求。
性能优势:
头部删除无需移动元素(优于vector)
内存连续分段存储(空间利用率高于list)
缺陷:不支持随机访问和高效遍历,但队列操作无需这些功能。
3. 可替换的底层容器
用户可通过模板参数指定其他容器(如list),但需满足以下条件:
支持back()、push_back()、pop_front()等操作
典型替代方案:
list:牺牲空间效率换取稳定的O(1)操作
自定义容器:需实现队列所需的接口。
4. 核心操作实现
push():调用底层容器的push_back()
pop():调用底层容器的pop_front()(deque专有优化)
front():返回底层容器的front()引用。
5. 与priority_queue对比
queue严格遵循FIFO,而priority_queue基于堆实现优先级排序(默认最大堆)
两者均为适配器,但底层依赖不同(priority_queue默认用vector+堆算法)。
总结
queue通过封装deque等容器,以最小开销实现了线程安全的FIFO数据结构,适用于需要严格顺序处理的场景(如任务调度、消息传递)。
list
#include<iostream>
#include<list>
using namespace std;
void PrintList(const list<int>& L){
for(list<int>::const_iterator i=L.begin();i!=L.end();i++){
cout<<*i<<" ";
}
cout<<endl;
}
void Test(){
list<int>l1;
l1.push_back(3249);
l1.push_front(1243);
PrintList(l1);
list<int>l2=l1;
l2.pop_front();
l2.pop_back();
l1.insert(l1.begin(),123);
l1.erase(l1.end());
l1.remove(123);
l1.reverse();
l1.sort();//升序排列
l1.sort(MyCmp);
}
bool MyCmp(int a,int b){
return a>b;//降序
}
pair
#include<iostream>
using namespace std;
void Test(){
pair<string,int>p("大海",132);
pair<string,int>p2=make_pair("东海",314);
}
priority_queue
对于自定义类型,需要重载<运算符指定排序规则。返回自己<他人是降序,自己>他人是升序。下面是按val升序。
struct Status {
int val;
ListNode *ptr;
bool operator < (const Status &rhs) const {
return val > rhs.val;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/merge-k-sorted-lists/solutions/219756/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
函数对象、谓词
谓词:返回bool的函数对象
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class Greater5{
public:
operator()(int a){
return a>5;
}
};
class MyCmp{
public:
bool operator()(int v1,int v2){
return v1>v2;
}
};
void Test(){
vector<int>v={31,42,523,123,52};
vector<int>::iterator i=find_if(v.begin(),v.end(),Greater5());
if(i==v.end()){
}
sort(v.begin(),v.end(),MyCmp);
}
内建函数对象
要#include<functional>
算数仿函数
#include<iostream>
using namespace std;
#include<functional>
void Test(){
negate<int>n;
cout<<n(50)<<endl;
plus<int>p;
int a=p(312,41);
//还有minus, multiplies, divides, modules
}
关系仿函数
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<functional>
void Test(){
vector<int>v{321,423,436,13,52,51,62);
sort(v.begin(),v.end(),greater<int>());
}
//equal_to, not_equal_to, greater, greater_equal, less, less_equal
问题
指针是内存地址,为什么声明指针还要声明类型?内存地址不是没有类型吗?deepseek回答
这个问题的核心在于理解内存地址的本质和编程语言的类型系统之间的区别。让我为你详细解释:
1. 内存地址的本质(硬件层面)
- 内存地址本身确实是无类型的,它只是一个数字(比如
0x7ffeeb0b9bdc),代表内存中的一个位置 - 在硬件层面,CPU 读取内存时并不关心数据类型,它只是按照指令访问特定地址
- 但是:内存中的数据本身是有含义的!同样的二进制序列:
可能是整数0x00000041 (十六进制)65,也可能是字符'A',还可能是浮点数1.14e-43
2. 指针类型的必要性(软件层面)
指针的类型声明是为了告诉编译器如何解释和处理该内存地址:
(1) 类型安全
int num = 42; double* p = # // 编译器报错:类型不匹配
- 防止意外错误访问(如把整数地址当作浮点数访问)
- 编译时就能捕获大量类型错误
(2) 解引用语义
int a = 10; int* p_int = &a; char* p_char = (char*)&a; printf("%d", *p_int); // 正确:读取4字节整数 → 10 printf("%d", *p_char); // 读取1字节 → 10 (但含义完全不同)
- 指针类型决定了读取多少字节(
int*读4字节,double*读8字节) - 决定了如何解释这些字节(整数?浮点?结构体?)
(3) 指针算术运算
int arr[5] = {0}; int* p = arr; p++; // 前进sizeof(int)=4字节
p+1实际增加的地址值 =sizeof(指向类型)- 如果没有类型信息,
p++该前进多少字节?
(4) 函数指针调用
cCopy Code
void (*func_ptr)(int); // 必须声明参数类型 func_ptr = &my_function; func_ptr(42); // 编译器知道如何传递参数
3. 特殊案例:void* 万能指针
C/C++ 中有一种无类型指针:
void* p = malloc(100); // 分配100字节
- 优点:可以指向任何数据类型
- 缺点:
- 不能直接解引用(编译器不知道如何解释)
- 使用时必须强制转换:
int* int_p = (int*)p; *int_p = 42; - 常见于底层系统编程(如内存管理)
4. 类型系统的哲学意义
类型系统本质上是赋予二进制数据以语义:
- 硬件视角:内存只是0/1序列
- 软件视角:我们需要知道:
- 这段数据是整数还是字符串?
- 它占多少字节?
- 对它进行
+1操作意味着什么?
图解说明
textCopy Code
内存地址:0x1000 0x1001 0x1002 0x1003 二进制数据:01000001 01000010 01000011 01000100 char* 解释: 'A' 'B' 'C' 'D' → 字符串 "ABCD" int* 解释: 0x41424344 (十六进制) → 十进制整数 1,094,796,612
总结
| 特性 | 内存地址 (硬件) | 指针 (软件) |
|---|---|---|
| 本质 | 无类型的数字 | 带类型的内存引用 |
| 目的 | 标识位置 | 安全访问数据 |
| 操作 | 直接读写 | 类型检查+解引用+运算 |
| 必要性 | 物理存在 | 防止错误+提供语义 |
指针类型是编程语言在无类型的内存物理层之上建立的类型安全层,它:
- 确保数据被正确解释
- 防止非法内存操作
- 使指针算术有意义
- 提供编译时类型检查
这就是为什么虽然内存地址本身无类型,但我们仍需声明指针类型——它是连接底层硬件和高级抽象的桥梁。
C++变量名编译后还存在吗
根据编译原理和程序执行机制,C++变量名在编译后的可执行文件中不存在,其作用仅限于编译阶段。以下是具体分析:
🔍 一、变量名的编译期作用
-
符号表映射
编译器在编译时将变量名转换为内存地址或寄存器编号,生成汇编/机器指令时仅保留地址信息而非名称本身16。
示例: 代码int x = 5;编译后可能变为mov [ebp-4], 5(ebp-4即x的栈地址)。 -
内存分配依据
变量名用于确定存储区域(栈、堆、静态区)和空间大小,但分配完成后名称即被丢弃24。- 局部变量:函数运行时在栈帧中动态分配,编译时确定偏移量4。
- 全局/静态变量:编译时分配在静态存储区,链接阶段解析地址5。
⚙️ 二、运行期的变量表现
-
无名称存储
程序执行时内存仅存储二进制数据,变量通过硬编码地址访问。例如:- 栈区:通过基址寄存器(如
ebp)加偏移量定位局部变量46。 - 全局区:直接使用绝对地址引用5。
- 栈区:通过基址寄存器(如
-
调试信息的例外
调试版本(Debug)可能保留变量名符号表,但仅用于开发工具(如GDB),不影响程序实际执行1。
📊 三、变量处理阶段对比
| 阶段 | 变量名是否存在 | 作用 | 存储形式 |
|---|---|---|---|
| 源代码 | ✅ 是 | 程序员可读标识符 | 文本字符 |
| 编译期 | ️ 临时保留 | 生成地址映射表 | 符号表(临时) |
| 可执行文件 | ❌ 否 | 被替换为地址/寄存器编号 | 二进制指令 |
| 程序运行时 | ❌ 否 | 通过内存地址直接访问数据 | 寄存器/内存单元 |
💎 结论
变量名是编译器与开发者的中间媒介,最终程序执行完全依赖硬件地址操作数据14。若需保留名称信息(如反射机制),需额外实现元数据系统,而非语言原生特性。
<<重载为成员函数的问题

在类内重载了+、<<,没有被识别。原来是在类内重载第一个参数一定是对象的指针。把上面第一行改成(&m1+453)就不报错了。
在C++中,将<<运算符重载为成员函数会导致编译错误,因为流插入运算符的第一个操作数必须是ostream对象,而成员函数的隐式this指针会占据第一个参数位置,这与运算符的天然调用方式冲突13。
核心问题分析
-
成员函数限制
成员函数形式的operator<<会隐式将this作为左操作数,导致无法匹配cout << myInt的调用形式(需要ostream&作为第一个参数)36。 -
正确实现方式
- 必须声明为全局友元函数,接受
ostream&作为第一个参数 - 函数签名应为:
cppCopy Code
friend std::ostream& operator<<(std::ostream& os, const MyInt& obj);
- 必须声明为全局友元函数,接受
修正方案对比
| 错误实现(成员函数) | 正确实现(全局友元) |
|---|---|
| ```cpp | ```cpp |
| class MyInt { | class MyInt { |
| ostream& operator<<(ostream& os); // 错误 | friend ostream& operator<<(ostream& os, const MyInt&); |
| }; | }; |
| ``` | ``` |
具体解决步骤
-
头文件修改
在MyInt.h中声明友元函数:cppCopy Code
class MyInt { int value; public: friend std::ostream& operator<<(std::ostream& os, const MyInt& obj); }; -
实现文件补充
在MyInt.cpp中实现运算符:cppCopy Code
std::ostream& operator<<(std::ostream& os, const MyInt& obj) { return os << obj.value; // 访问私有成员需友元声明:ml-citation{ref="1,6" data="citationList"} } -
使用示例
cppCopy Code
MyInt num(42); std::cout << num; // 正确调用全局运算符:ml-citation{ref="3,4" data="citationList"}
对于模板类,实现需直接写在头文件中以避免链接错误4。若必须保持成员函数形式,可通过转换函数间接实现,但会牺牲代码直观性6。
一个在类MyInt内重载的运算符+,第一个参数是this指针,如果传给它一个MyInt对象会怎么样
在C++中,当运算符+作为成员函数重载时,其第一个参数隐式由this指针传递,此时若传递MyInt对象会引发以下情况:
-
参数传递机制
成员函数形式的operator+实际只需显式声明一个参数(右操作数),左操作数通过this指针隐式传递1。例如:cppCopy Code
MyInt operator+(const MyInt& right) { return MyInt(this->value + right.value); }调用时
obj1 + obj2会被转换为obj1.operator+(obj2),其中obj1通过this传递,obj2作为显式参数2。 -
与全局重载的区别
特性 成员函数重载 全局函数重载 参数数量 显式1个(隐式含 this)显式2个 访问权限 可直接访问私有成员 需友元声明访问私有成员 调用方式 obj1 + obj2operator+(obj1, obj2) -
使用限制
- 无法实现
42 + obj这样的表达式,因整数无法调用成员函数3 - 若需支持交换律,必须额外定义全局重载版本4
- 链式运算(如
a + b + c)需返回新对象而非引用以避免悬垂引用5
- 无法实现
-
典型应用场景
cppCopy Code
class MyInt { int val; public: MyInt(int v) : val(v) {} // 成员函数重载 MyInt operator+(const MyInt& other) const { return MyInt(val + other.val); } };调用时编译器会自动处理
this绑定,确保操作数位置正确
总结
类MyInt内部重载+,+左边的对象变成+所属的对象,第一个参数变成+左边对象的指针。表面上调用和参数列表对不上,但成员类+本来就不是按+左右的对象匹配参数列表调用的,而是翻译成
左边对象.operator+(右边对象)
所以没问题。
使用全局函数重载<<运算符,且支持的类在头文件里
如果把<<重载的实现放在支持的类的头文件,则fatal error LNK1169: 找到一个或多个多重定义的符号

如果把<<重载的实现放在B.cpp,声明在B.h,主脚本开头#include "B.h"则LNK1120: 1 个无法解析的外部命令

把B.cpp从项目中排除,再加回来,就tm好了。

正确做法:把<<重载的声明放在头文件,定义放在主函数cpp
或在头文件定义,开头加inline
在头文件里的<<运算符重载函数,不加inline就会报错

.h、.cpp、.hpp
我们知道C++提倡把声明放.h,实现放.cpp,开头#include .h。
#include .cpp会怎么样?
报重复定义错误。项目会编译所有cpp,#include cpp会再编译一次,导致重复定义。

.h没人#include就不编译?
不编译。
.h被多个文件包含怎么样
如果开头有
#pragma once
或者被包裹
#ifndef XXX
#define XXX
//头文件内容
#endif
没问题,没有就也报错。
.cpp开头写#pragma once,然后#include .cpp怎么样
报错。#pragma once只在一个cpp内有效,一个cpp包含任意层.h也可以。
我们想象C++环境有一个字典,记录所有全局变量、全局函数、类,定义相当于往字典加东西,C++的机制是往字典加重名的东西(包括函数和变量重名、函数重名且参数列表相同,函数重载允许)就报错。声明相当于预定从字典读取东西。
C++有这个问题因为
- #include是复制文本;
- 让脚本自己去取自己引用的;
C#的依赖是csproj引用程序集,是项目级别的操作。相当于一组人要用A物品,每个组员cpp自己去取>A被领取多份,让小组管理员csproj去取,组员cs找管理员使用>A只领取一份。

#define和typedef
在编程时#define用前面的代替后面的,typedef用后面的代替前面的。
#define和typedef是C/C++中用于定义别名的两种机制,但它们在原理、作用域和类型检查等方面存在显著差异:
1. 原理与处理阶段
#define:
是预处理指令,在编译前进行简单的文本替换,不进行语法或类型检查。
例如:#define INT int会将代码中所有INT替换为int。
typedef:
是关键字,在编译阶段处理,为已有类型创建别名,具有类型检查功能。
例如:typedef int Integer;将Integer定义为int的别名。
2. 语法与作用域
#define:
无需分号结尾,作用域为全局(除非用#undef取消)。
示例:#define PI 3.14。
typedef:
需分号结尾,遵循C/C++作用域规则(如局部或文件作用域)。
示例:typedef struct { int x; } Point;。
3. 类型安全性
#define:
易因简单替换导致错误。例如:
cpp
Copy Code
#define PTR int*
PTR a, b; // 展开为`int* a, b;`,`b`实际为`int`而非指针:ml-citation{ref="9,12" data="citationList"}。
typedef:
保证类型一致性。例如:
cpp
Copy Code
typedef int* IntPtr;
IntPtr a, b; // `a`和`b`均为`int*`:ml-citation{ref="9,19" data="citationList"}。
4. 典型应用场景
#define适用场景:
定义常量(如#define MAX 100)。
条件编译(如#ifdef DEBUG)。
简单宏函数(需注意括号避免优先级问题)。
typedef适用场景:
简化复杂类型(如函数指针、结构体)。
跨平台类型兼容(如typedef long double REAL)。
5. 与const结合时的差异
#define:
const修饰替换后的文本。例如:
cpp
Copy Code
#define T char*
const T p; // 展开为`const char* p`,`p`为指向常量的指针:ml-citation{ref="15" data="citationList"}。
typedef:
const修饰类型别名整体。例如:
cpp
Copy Code
typedef char* T;
const T p; // `p`为常量指针(`char* const`):ml-citation{ref="15,16" data="citationList"}。
总结
优先使用typedef:
需要类型安全、作用域控制或复杂类型定义时。
谨慎使用#define:
仅适用于简单替换或条件编译,需注意括号和副作用。
delete完指针的值是?
在C++中,使用delete释放指针后,指针的值和行为具有以下特性:
-
指针值保持不变
delete操作仅释放指针指向的内存空间,但不会修改指针变量本身存储的地址值,指针仍保留原内存地址34。 -
成为野指针(悬垂指针)
释放后的指针称为野指针,其指向的内存可能被系统重新分配,继续访问会导致未定义行为(如数据损坏或程序崩溃)12。 -
重复释放风险
若未将指针置为nullptr,多次调用delete同一指针会引发双重释放错误,可能导致堆损坏56。 -
内存残留现象
部分环境下,已释放内存的内容可能暂时未被覆盖,此时通过野指针仍可能读取到原数据,但这是不安全且不可靠的行为29。 -
最佳实践
建议在delete后立即将指针赋值为nullptr,既可避免野指针问题,又保证后续delete nullptr操作的安全性(C++标准规定删除空指针无副作用)
delete不会把那块内存修改成默认值?
在C++中,delete操作不会自动将被释放的内存修改为默认值,其行为如下:
-
内存内容保留
delete仅将内存标记为"可重新分配",不会主动清零或修改原有数据。原内存区域的内容可能暂时保持不变,直到被其他程序或新分配覆盖。 -
访问风险
虽然内容可能暂时可见,但访问已释放内存属于未定义行为(UB),可能导致程序崩溃或数据污染。
“可重新分配”标记记录在哪?
在C++中,动态内存的“可重新分配”标记由堆管理器(Heap Manager)维护,其具体存储机制如下:
-
堆管理器的元数据区
堆管理器会在分配的内存块前后插入额外的控制信息(如块大小、状态标记等),这些元数据通常存储在独立于用户数据的隐藏区域,用于跟踪内存块是否可被重新分配78。 -
标记的实现方式
- 位图(Bitmap):部分实现使用位图标记内存块的分配状态,每位对应一个内存块,0表示空闲(可分配),1表示已占用7。
- 链表结构:更常见的实现是通过双向链表连接空闲内存块,每个空闲块头部存储指向相邻块的指针,形成“空闲链表”。释放内存时,堆管理器将块重新链接到链表中标记为可用710。
-
调试扩展
调试模式下,分配器可能添加额外标签(如头部/尾部标记)检测越界访问,此时“可重新分配”状态可能与其他调试信息(如分配时间戳)共存10。 -
系统级依赖
具体实现因操作系统和编译器而异,例如:- Windows的堆管理器使用
_HEAP_ENTRY结构存储状态标记7。 - Linux的glibc通过
malloc_chunk结构管理内存块,其中size字段的低位比特用于标记分配状态78。
- Windows的堆管理器使用
关键点:这些标记对用户透明,直接操作可能破坏堆结构。安全的内存管理应依赖语言提供的接口(如new/delete)或智能指针9。
内存里记录可用内存的地址和大小用什么数据结构?
内存管理中记录可用内存地址和大小的数据结构通常采用以下几种形式:
空闲链表(Free List)
通过双向链表连接所有空闲内存块,每个节点存储块起始地址和大小,支持快速合并相邻空闲块。例如glibc的malloc_chunk结构包含prev_size和size字段,并通过fd/bk指针维护链表。
位图(Bitmap)
用二进制位标记内存单元是否空闲,适用于固定大小的内存池管理。每位对应一个内存单元,1表示占用,0表示空闲。位图可实现O(1)时间复杂度的状态查询。
伙伴系统(Buddy System)
将内存划分为2的幂次方大小的块,通过树状结构管理空闲块。合并和拆分时快速定位相邻块,减少碎片。
边界标记法(Boundary Tags)
在每个内存块首尾存储块大小和状态标记,便于合并时快速定位相邻块。如malloc_chunk的prev_size字段用于前向合并。
分离空闲链表(Segregated Free Lists)
按不同大小类别维护多个空闲链表,提升分配效率。例如TCMalloc采用此结构优化小对象分配。
这些结构通常由堆管理器实现,如glibc的ptmalloc或Windows的HeapAlloc,用户通过malloc/new等接口间接使用。
怎么判断一个父类变量装的是某种子类?
在C++中判断父类指针/引用实际指向的子类类型,主要有以下几种方法:
一、typeid运算符(需包含<typeinfo>)
-
基本用法
直接比较typeid返回的类型信息,但存在多态失效问题:当父类指针指向子类对象时,若父类无虚函数,typeid仍返回父类类型12。 -
多态场景要求
必须确保父类至少有一个虚函数(如虚析构函数),才能正确识别动态类型25。
cppCopy Code
if(typeid(*parentPtr) == typeid(ChildClass)) { // 实际为ChildClass类型 }
二、dynamic_cast动态转换(推荐)
- 安全向下转型
尝试将父类指针转换为子类指针,失败返回nullptr。要求父类必须包含虚函数且开启RTTI39。
cppCopy Code
ChildClass* childPtr = dynamic_cast<ChildClass*>(parentPtr); if(childPtr != nullptr) { // 转换成功,确认是ChildClass类型 }
三、自定义类型标识(无RTTI时)
- 虚函数方案
在基类定义虚函数返回类型标识(如枚举值),子类重写该函数返回特定标识25。
cppCopy Code
class Base { public: virtual int getType() const = 0; }; class Derived : public Base { int getType() const override { return 1; } };
四、方法对比
| 方法 | 优点 | 缺点 |
|---|---|---|
typeid | 语法简单 | 需多态支持,可能误判1 |
dynamic_cast | 类型安全,标准推荐 | 性能开销较大9 |
| 自定义标识 | 不依赖RTTI,可定制 | 需手动维护类型系统2 |
最佳实践:优先使用dynamic_cast进行运行时类型检查,若性能敏感且类型系统稳定可采用自定义标识方案34。

2153

被折叠的 条评论
为什么被折叠?



