目录
结构体内存对其规则
相关面试题
this指针
相关面试题
右箭头选择运算符->
C语言和C++实现Stack的对比
结构体内存对其规则
1、第一个成员在与结构体偏移量为0的地址处
2、其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认对齐数与该成员大小的较小值(vs中默认为8)
3、结构体总大小为:最大对齐数的整数倍
4、如果是嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有结构体的最大对齐数的整数倍
相关面试题
1、内存对齐会导致空间闲置,那为什么要进行内存对齐?
①提高读取效率
②硬件(地址总线)规定内存一次读取4(32根线)或8(64根线)个字节
③如果不内存对齐可能会对同一块内存需要多次读取
采用内存对齐:一次读取4个字节,先读取一个字节的i后将其它三个没用的字节丢掉,然后继续读取四个字节的a
不采用内存对齐:一次读取4个字节,先读取i的一个字节和a的前三个字节后将a的前三个字节丢掉,然后继续读取a剩余的一个字节,最后将那三个字节和最后一个字节组合才能得到a,麻烦
2、如何让结构体按照指定的对齐参数进行对齐?
答:#pragma pack(指定对齐数)
this指针
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//声明
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 27);
d2.Init(2022, 2, 28);
d1.Print();
d2.Print();
return 0;
}
Data类中有Init和Print两个成员函数,不论是d1还是d2它们调用的Init函数和Print函数在实现上是一样的,为什么编译器可以做到d1调用Print函数就会打印d1对象的成员变量组成的日期,d2调用Print函数就会打印d2对象的成员变量组成的日期?
这是因为每一个成员函数都会多一个叫this的指针,它是成员函数的第一个参数,那么对于Print函数其实我们可以将其理解成:
void Print(Data* this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
void Print(Data* this,int year,int month,int day)
{
_year = year; //实际上就是this->_year = year;
_month = month; //this->_month = month;
_day = day; //this->_day = day;
}
在调用的时候,编译器也会将其处理成这样的形式:
d1.Print(&d1);
d2.Print(&d2);
即将调用对象的地址当作实参传递给函数(重点!!!)
~传入谁的地址就访问谁,但是这个传递是隐式的~
注意事项:
1、this指针实际上的格式是:类名* const this,即指向的内容可以更改,但本身不能修改
2、形参和实参位置不能显式的写this指针
3、函数内部可以使用this指针
相关面试题
1、this指针存在哪里?堆?栈?静态区?常量区?对象里面?
对象里面(×):计算类的大小时,有三个整型成员变量的类的大小是12字节,如果this指针存放在对象中类的大小应该是大于12的
常量区(×):被const修饰的变量不会存放在常量区,const char*p = "xzxxxxxx"中,p作为指针是存放在栈上的,"xzxxxxxx"是被存放在常量区的,这段代码的意思是将字符串存储的地址给了p,打印p显示出来的就是常量区的地址,被const修饰的变量i的地址和变量j的地址相近,由于变量j是存放在栈上的,可以推出被const修饰的变量i也是存放在栈上的
不加void*强转,p打印出来的就是字符串"xzxxxxxx" ,强转后打印的是字符串的地址
静态区(×):被static修饰的变量和全局变量才存放在静态区
堆(×):calloc、malloc、realloc开辟的才在堆上
栈(√): this指针是成员函数的形参,而形参跟指针一样是存放在栈上的
有些编译器(比如vs)会将this指针存放在寄存器上,频繁访问this指针时效率更高
2、下面程序编译运行结果是: A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
指向A类的指针p被初始化为了空指针,由于成员函数Print不在A类的对象中,故当p访问Print函数时,并不会解引用p获得p中存放的地址(获得了也没用,Print函数根本不在p所指向的那片空间上,对此编译器表示不做无用功)通过汇编指令我们也可以发现这里执行的是将p指针中存放的值存入rcx寄存器中,然后将寄存器 RCX 的值写入到栈上偏移为 8 字节处,对于Print函数汇编指令就直接是call Print的地址并没有说在p指向的空间中寻找该函数,从侧面印证如果指针解引用后要访问的空间不在指针指向的空间上指针不会解引用的结论:
实际上这一步就是向this指针传值(nullptr,因为p是空指针),因为在之前说过d1.Print(),其实括号内还有一个隐藏传递的内容&d1,只有这样编译器才能分辨到底要打印哪个对象的内容,d1.Print() => d1.Print(&d1)、(*p).Print() => (*p).Print(*p)
有空指针,传递了空指针,但是没有对该空指针进行解引用(因为没必要)所以没有问题
我们是好朋友(可解引用),(一定吗)我们是好朋友吗?有钱(非空)的时候才是
结论:有->编译器在编译时不一定会让它成功的解引用,只有当指针试图访问的内容的确在指针所指向的那片空间上,才会进行解引用
3、下面程序编译运行结果是: A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
①p仍然是空指针,仍然不会进行解引用,向this指针传递的仍然是空指针
②但是,由于_a(A类的成员变量)是在p所指向的空间的,所以会发生解引用
③在打印_a的时候,实际上的代码应该是this->_a,即(*this)._a,而this指针是空指针,对空指针的解引用会报错
右箭头选择运算符->
基本概念:利用指针访问结构中的成员是很普遍的,因此C语言专门提供了一种运算符:->
注意事项:
1、运算符->是运算符*和运算符.的组合
new_node->value = 10; => (*new_node).value = 10;
先对new_node解引用(获取该指针所指向内存地址上存储的值)从而找到它所指向的结构,然后再选择结构或对象中的成员value
2、由于运算符->产生左值,所以可以在任何允许普通变量的地方使用它
C语言和C++实现Stack的对比
C语言实现:
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array,
newcapacity*sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
在使用C语言实现时,Stack相关操作函数有以下特性:
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用时必须传递Stack结构体变量的地址
结论:结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式分离,而且实现上较为复杂,涉及到大量指针操作,稍不注意就会出错
C++实现:
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top(){ return _array[_size - 1];}
int Empty() { return 0 == _size;}
int Size(){ return _size;}
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity *
sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++中可以通过类将数据以及操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,而且每个方法都不需要传递Stack*的参数了,编译器编译后该参数会自动还原,即C++中的Stack*参数是编译器维护的,C语言中需要用户自己维护(看起来是s.Init或者s.Push(1),实际上却是s.Init(&s)和s.Push(&s,1))
~over~