c++底层原理


前言


进阶学习面向对象C++语言的底层知识;学习以下内容需要有一定的编程基础;但如果认真去理解相信肯定会有很多收获;

一、结构体

1. 定义

是C++的一个数据类型,与int、char、float等同一等级;

2. 结构体表现形式

1)结构体中可以存储变量、函数等;如果函数内容比较多,则会使得结构体看起来非常庞大,因此函数一般在结构体里声明,实现是写在结构体外面;
2)结构体可以声明变量、函数以及继承其他结构体;
3)在存储、创建、调用、继承过程中,结构体除了在权限上有区别,其余地方和类都是一样的。

3. 结构体作用

结构体存储的数据类型可以是丰富多样的,这就弥补了存储多个数据但得同类型的数组的不足;

4. 结构体特点

1)结构体创建后是保存在堆中的;
2)结构体作为参数传入函数时,首先会将结构体所有内容复制一遍一个个压入栈中;若作为返回值时,会从栈中复制一遍取出来;结构体定义不要定义为局部变量;
3)由于2的特点传结构体或返回结构体时使用指针的形式,那么函数内部使用的结构体内容还是堆中的,只是通过指针指向的结构体首地址去堆中取。这样传将有利于资源合理利用。

5.结构体字节对齐

聚焦问题:字节对齐是一个空间和时间的问题,一字节对齐不会有空间浪费,但在搜索时间上会比较慢;若选择合适的对齐空间,可以节省搜索时间;
本机宽度:本机有32位、64位等等的差别,不同宽度对齐最优对齐参数不同;
对齐参数:结构体对齐数是选择成员宽度和设置对其参数值小的进行对齐,确定对齐数后结构体内存必须是对齐数的整数倍。对齐参数可以取值为1、2、4、8,默认为8,代码如下:

#pragma  pack(n)
//放结构体
#pragma pack()

对齐原则:
–结构体第一个数据成员放在offset为0的位置,后面的数据成员存储的起始位置要从该成员的整数倍开始;
–结构体的总大小必须是其内部最大成员的整数倍,不足的要补齐;
–如果结构体中包含结构体,结构体成员要从内部最大元素大小的整数倍地址开始存储;
–对齐参数如果比结构体成员的sizeof值小,该成员的偏移量应该以此值为准;

//例子1(原则1、原则2):
struct A{
int s;
char b;
short c;
}
//若默认对齐参数是8,结构体存储结构如下(由于8小于4,选择小的进行对齐),大小为8字节;
//s,s,s,s
//b,c,c,0
//例子2(原则1、原则2):
struct A{
char b;
int s;
short c;
}
//若默认对齐参数是8,结构体存储结构如下,大小为12字节;
//b,0,0,0
//s,s,s,s
//c,c,0,0

结论:结构体存储同样的数据类型和元素个数;只是写的先后顺序不一样,导致结构体的占用空间不同;

//例子3(原则3)
struct B{
	int c;
	double d;
}
struct A{
	int i;
	B j;
	char f;
}
//结构体A的存储结构如下,占用空间大小为24;
//i,i,i,i,c,c,c,c
//d,d,d,d,d,d,d,d
//f,0,0,0,0,0,0,0
6. 结构体和类的区别

结构体和类的区别主要是权限。权限体现在继承和声明成员;结构体声明或继承的所有成员默认都是public,类声明或继承的成员默认都是private。
上面说的都是在C++中结构体的限制,但在C中结构体不允许包含函数,访问权限只能是publi且不支持修改,面向过程语言当然也就不可继承。当然这些只是特性,而特性的功能是可以通过c语言代码去实现的,如下可以使用函数指针实现多态功能

//使用A结构体创建多个实例,不同实例调用一个名称相同的函数指针,然后实现不同的函数功能;
// CMakeProject1.cpp: 定义应用程序的入口点。
//
using namespace std;
typedef int Func(char*);
struct A {
	int b;
	Func* func;
};
A* create(int data, Func* funcObj) {
	A* structObj = (A*)malloc(sizeof(A));
	structObj->b = data;
	structObj->func = funcObj;
	return structObj;
}
int run1(char* str) {
	cout <<"第一个输出结果"<< str << endl;
	return 0;
}
int run2(char* str) {
	cout <<"第二个输出结果"<< str << endl;
	return 0;
}
void delete_A(A* structObj) {
	if (structObj != NULL) {
		free(structObj);
		structObj = NULL;
	}
}
int main() {
	A* structObj1 = create(10, run1);
	A* structObj2 = create(20, run2);
	structObj1->func("hello");
	structObj2->func("world");
	delete_A(structObj1);
	delete_A(structObj2);
	return 0;
}

二、指针

  • 内存分布:
    代码区:代码;
    栈:参数、局部变量;
    数据区:主要有全局变量区和常量区,其中全局变量存储全局变量,是可读,可写的;常量区存储的是常量,是可读不可写。
//【“china"是存储在常量区的,这时x存储的是"China"的地址,*(x+1)="s"会报错】
char* x = "china"
//【"china"也是存储在常量区,但是在编译是会将每个字符从常量区拷贝到堆栈里,因此y[0]="s"是正确的】
char[] y = "China" 
  • 代码解释
    所有的代码通过编译器后转成了汇编。每行代码都有对应的硬编码,硬编码是存储在一块地址中的,每个硬编码通过反汇编引擎翻译成汇编语句。
1.指针函数

定义:返回值是指针的函数。

2.函数指针

1)定义
每个函数都会存放在一块地址中,一个指针变量去指向这块地址的首地址,而这个指针变量就称为函数指针;
2)声明

int (*pFun)(int,int);

3)特性
-pFun的宽度:4
-赋值:

pFun = (int (*)(int,int)10;

-运算:
函数指针不能做++,–,相减。因为函数体内部代码不同,其*pFun宽度不同,但可以做比较;
4)作用
----加载动态链接库(DLL);首先通过逆向分析出来了DLL库中的函数地址、返回值和参数,然后就可以通过指针函数去调用该函数;
----将硬编码复制到数据区,然后定义已给函数指针,给指针赋值硬编码的首地址,再通过函数指针调用即可;
----在对函数代码进行加密,自己保存一串密钥,使其对硬编码进行数据运算,解密时使用密钥进行反运算即可;

3.数组指针

例子1:char* arr[5];
解释:这里arr是一个数组,数组中的每一个数据都是char*类型,一个char*类型占用4个字节的空间用于存储地址,那么这个数组总共占用20个字节;

例子2:定义是:char (*px)[5];赋值时:px = (int (*)[5])10;
解释:变量为px,*px存储的是一个5个char类型的数组首地址,sizeof(px)则为4;px+=3后px的十进制为1x5x3=15;
这里的*px和px联系与区别:*px和px的地址是一样的,他们都是指针,但是他们的宽度不同,px的宽度是5,*px的宽度是1;
作用:这种写法就可以将一维数组以二维数组的方式操作;

例子3:定义是:char (*px)[2][4];求*(*(*(p+2)+3)+4)
解释:变量为px,宽度为2x4x1 = 8,*p的宽度是4x1 = 4,**p的宽度则为1,因此*(*(*(p+2)+3)+4)访问的值是从首地址往后偏移8x2+4x3+1x4=32个字节;

总结:例子1表示是数组中存储的指针,例子2和例子3是指针数组(用指针操作多维数组)。

4.结构体指针

1)例子:假设student是一个3个int类的结构体;student* A=(student*) 100,进行A++运算后,十进制打印A是112,若student** A=(student**) 100,进行A++运算后,十进制打印A是104;
解释:student结构体存储的宽度为12,在A进行运算时去掉一个星则类型变为student,此时100+12=112;同样student**变为student*,此时student*宽度是4,所以A++后,A的十进制为104;

2)结构体指针存储的不一定只能存结构体,例如:student是一个包含int、char、short类型的结构体;此时
int data = 100;
student* A = (student*)data;
print(“%d,%d,%d”%(A->one,A->two,A->three))
输出第一个值是100,但第二第三个值是一个乱码;
解释:A存储的是将data作为首地址,取student宽度的地址范围作为*A的可访问的地址;此时只有第一个4字节地址是正确的后面的所有地址存储的都是乱序的。

3)底层调用约定主要有cdCall,stdCall,msCall三种:
cdCall:该调用约定主要有外平栈,参数从右往左传这两个特点;

5.多重指针

1)问题:char* A、char** B、char*** C他们之间有什么区别呢?
特点一:在指针运算时,类型需要去星,A++后时在A的初始地址上加上char类型的宽度(1字节);同理B++是在B的首地址上加char*类型的宽度(4字节),char*类型宽度也是4字节;
特点二:*取值时,类型需去星,如 *A的类型为char,*B的类型为char*类型,*C的类型则为char**类型;char*、char**、char***等这些指针的宽度都是4字节,因为这里指针指向的内存所存储的都是地址。

三、类

(一)this指针

1. 定义:

this是一个指针,该指针只做一件事就是指向对象首地址。

2. 表现形式:

1)创建类对象是编译器会默认生成一个this指针,在类内部可以使用this指针去获成员变量和函数;
2)若类中有成员函数,编译器会将this指针作为成员函数的第一个参数传入;

3. 作用:

1)区分那些是参数,哪些是成员;
2)可以返回当前对象的首地址;
3)编译器不允许使用者对this指针赋值;

(二)类的控制权限

1.定义:

类等控制权限有三个关键字,分别是private(本类成员可以使用)、protect(子类成员可使用)、public(其他任何地方都可以使用);注意在类外面声明并定义的一个函数,该函数里面创建了类对象,此时不能通过对象去访问private或protect中的内容。

2.作用:

1)方便编写者管理。一般public中的内容不能随意改动,若有不确定的内容可以写到private中,后续更改时也不用改其他类使用该成员的代码。

四、继承

1.定义:

是c++类的一个特性,旨在复用其他类的属性和功能;其他类称为该类的父类,该类为子类。

2.作用:

复用其他类的属性和功能,减少编写代码的时间成本,方便管理和维护。

3.机制:

1)继承本质就是复制。子类继承父类,实际上将父类所有的成员复制到子类;
2)继承时会有一个控制权限,若未选择继承权限,那么继承的所有成员都是private成员,尽管父类中有声明是protect或public;

五、多态

(一)虚函数

1. 定义:

虚函数是出现在类中的,是一个用于面向对象的多态的,有虚函数和纯虚函数两种。虚函数是父类声明并实现,子类可重写;纯虚函数是父类声明不可实现,子类必须实现。

//声明并实现
virtual int run(){
	cout<<"实现"<<std::endl;
	return 0
};
//仅声明,需子类实现;
virtual int run1()=0;
2. 虚函数的作用:

虚函数可以帮助基类实现对子类成员虚函数的灵活调用;从而实现类的多态特性。

3. 虚函数底层:
  • 虚函数的位置:
    首先一个类创建时,都会有已给this指针,这个指针指向地址所存的东西即是该类下所有内容,若类中有虚函数,那么该内容的首地址所存储的内容将是一个虚函数表,虚函数表中存的东西就是虚函数的地址。
  • 虚函数的特点:
    1)不管一个类中有多少个虚函数,this指针下存的都只有一个虚函数表的地址;
    2)使用对象"."这种形式调用的话,虚函数和普通函数在底层的表示都一样(都是直接调用形式);若使用对象指针的形式去调用,则虚函数是间接调用,普通函数是直接调用;
    3)调用时可以通过指针去取出要调用的虚函数地址(该地址是一个4字节,32位系统一般用int类型表达),然后创建一个函数指针,将虚函数地址的int类型强转为函数指针类型,再实行调用;
  • 虚函数表存储的虚函数内容:
    1)多继承无函数覆盖:若继承俩个父类,那就有两个虚函数表,第一个虚函数表只有第一个继承的虚函数地址和子类虚函数地址(每个虚函数表地址都是4字节);
    2)多继承有函数覆盖:覆盖的哪个虚函数,虚函数就在被覆盖的那个虚函数表里;
    3)多重继承无函数覆盖:只有一个虚函数表,子类虚函数表根据继承的顺序依次从基类到子类的顺序存储
    4)多重继承有函数覆盖:和多继承有函数覆盖同理;

(二)多态

1.定义

绑定:调用的代码与函数真正的地址关联的这个过程;
编译期绑定:在编译时就以及确定调用内容的真正地址。普通成员属性和函数都是编译时绑定;
动态绑定(运行期绑定):在运行时才能确定调用的内容的真正地址,虚函数会在运行时绑定(因为编译时绑定的是虚函数表地址,虚函数真正的地址在虚函数表中);
多态:动态绑定的一种形式多态,指的是类中成员函数可以有多种形态、行为(通过虚函数进行变换);

2.作用

1)若子类继承父类时,由于继承的特性,父类的属性会在子类地址的首位,指针指向的首地址就是父类的地址;
2)问题:若无虚函数就算子类重写父类的函数,通过子类去调用重写的函数,此时调用的只是父类被重写的函数;
3)解决方案:将重写的函数声明为虚函数;此时用子类指针调用则指向子类重写的函数,用父类指针调用指向的就是父类被重写的函数。
4)应用:多态用于析构函数,当用父类指针访问子类对象时,调用完子类对象后此时使用的析构函数将是子类的析构函数;

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值