C++:类和对象(从底层编译开始)详解[前篇]

embedded/2025/3/15 11:07:06/

目录

一.inline内联的详细介绍

(1)为什么在调用内联函数时不需要建立栈帧:

(2)为什么inline声明和定义分离到两个文件会产生链接错误,链接是什么,为什么没有函数地址:

二.类,实例化和this指针

1.类的介绍(class):

2.实例化:

(1)实例化的概念:

(2)实例化的空间分配:

3.this指针:

4.关于空指针访问成员变量的注意点:

三.类的默认成员函数

1.构造函数:

2.析构函数:

3.拷贝构造函数:

4.运算符重载:

四.日期类实现

一.inline内联的详细介绍

为了更清楚的明白类的定义与底层运行逻辑,我先从inline内联开始讲起:

• ⽤inline修饰的函数叫做内联函数编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率 //(1)为什么不需要建立栈帧

• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定,inline适用于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略(因为内联的展开在需要频繁调用短小函数的代码里,可以大限度上减少函数调用指令(call)的使用,而在其他函数体本身较大的情况下,inline不展开的调用指令的方法可能会显得更为简便) 

• C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数

• vs编译器debug版本下⾯默认是不展开inline的,这样方便调试

• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错//(2)为什么会产生链接错误,链接是什么,为什么没有函数地址

(内联的使用示例)

#include <iostream>// 显式声明为内联函数
inline int add(int a, int b) {return a + b;
}int main() {std::cout << add(3, 5) << std::endl; // 编译器可能将 add(3,5) 展开为 `3 + 5`return 0;
}

上述的阐述乍看总有种似懂非懂的感觉,但一旦深入想想就还有好些东西不明不白,下面我将对这段话中可能会产生的疑问做出一一解答:

(1)为什么在调用内联函数时不需要建立栈帧:

(a).在理解这个问题之前,我们需要先搞明白函数的栈帧是怎么一回事

       简而言之栈帧是程序执行过程中用于保存函数调用状态的临时数据结构,它在函数调用时被创建,返回时销毁。每个栈帧对应一次函数调用,记录了函数的执行上下文信息

以下这张图片就展示了函数Add在调用时所创建的栈帧,而其中的push等相关汇编命令我也附在下面:

        

(b).再让我们区别以下函数的栈帧整体代码的编译和链接的关系:

据上述代码编译的过程而言,函数栈帧的创建属于程序运行时的动态数据结构,虽与编译链接过程的静态代码无关,但编译与链接依旧会在其运行时对其产生影响:如编译阶段为栈帧的创建和销毁生成正确的指令,以及链接阶段确定函数的位置以及符号引用,因此,一般情况下较小的函数被inline展开时,其函数名并不会进入符号表,而是直接在调用处替换代码(发生在预处理中,编译之前),自然也就跟栈帧的创建销毁没啥关系了

(2)为什么inline声明和定义分离到两个文件会产生链接错误,链接是什么,为什么没有函数地址:

  

inline之所以不建议声明定义分离,是因为当我们假设在head.h头文件里定义了内联函数add(自定义函数名)然后分别在a.cpp里定义add函数然后在b.cpp里调用add函数然后运行,那么在对程序进行编译时,会发现对于add函数头文件里只有声明而没有定义,因此编译器会假设add为一个外部函数(这里类似于一般函数的跨文件调用),但与一般函数调用不同的是,一般函数在假设外部函数时会同时在符号表生成一个对函数的引用(包含了未解析的地址),然后再在链接过程中通过对各文件的链接重新补全符号表里未解析的地址,从而实现函数声明定义的分开,但inline函数却不一样,它同样会在符号表里生成一个未解析的地址,但由于inline函数的性质就是对函数体代码的整体替换从而实现对指令代码的节约使用而且需要明确的内联点才可以进行替换,因此这样导致了其无法在链接时找到对应的内联点进而不能像一般函数那样在链接过程中补全对应的符号表里未解析的地址(内联需要替换的代码都找不到更别说地址了),从而发生链接的报错

二.类,实例化和this指针

1.类的介绍(class):

其中有两点需要特别注意

(a) 类中的成员函数默认为内联

(b)关于访问限定符:

如下代码:public和private是访问限定符,在public后面的成员函数和成员变量可以直接在类的外部使用,private后面的成员函数和成员变量不能被直接使用。       

        通常我们把成员函数定义为public,把成员变量定义为private

#include<iostream>
using namespace std;
class TEST
{
public://成员函数void test(){return;}
private://成员变量int _a;int _b;
};
//以上class为类的关键字,TEST为类的名字,{}中的为类的主体//但同样的,C++由于相当于C的pro max版,同时也可以兼容C中的struct结构:
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}LTNode;int main()
{return 0;
}

关于类域

#include<iostream>
using namespace std;class TEST
{
public://成员函数声明int test(int a, int b);private://成员变量int _a;int _b;
};
//类定义了一个新的作用域,类的所有成员函数都在类的作用域中。在类体外定义成员时,需要使用类域名::来访问成员
//如果不指定类域的话,在定义函数时,程序在全局域找不到函数的声明就会报错。编译器不会主动去类域中寻找函数定义
int TEST::test(int a, int b)
{return a + b;
}
int main()
{TEST A;int c = 10; int d = 20;cout << A.test(c, d) << endl;return 0;
}

2.实例化:

(1)实例化的概念:

(2)实例化的空间分配:

 对象的大小只包含成员变量的大小,成员函数不占内存空间

打个比方,现在实例化出了两个类,分别为A,B但A和B的成员变量和地址是不同的,但如果访问这两个类的成员函数,他们都会链接到一个地址(只读存储区,静态存储),所以说我们sizeof(类对象)只用统计成员变量占用的空间

 成员变量占用的空间也符合内存对齐规则:

关于这个对齐其实有点比较容易遗忘,因此我再简述一下:

1. 基本概念
 对齐:数据类型的起始地址必须是该类型大小的整数倍
 例如: int (4字节)的地址必须是  0x4, 0x8, 0xC... 
 未对齐:数据起始地址不满足对齐规则,可能导致性能下降或硬件错误(如 ARM 架构)

2. 内存对齐规则
 a. 自然对齐
 规则:每个数据类型的地址必须是其自身大小的整数倍
 示例:

struct AlignedStruct {char a;        // 1字节 → 地址 0x0int b;        // 4字节 → 地址 0x4(填充3字节)double c;     // 8字节 → 地址 0x8(填充7字节)
};

b. 结构体对齐
成员顺序:成员按声明顺序排列,每个成员按自然对齐对齐

举例:

​
struct CompactStruct {int a;       // 0x0char b;      // 0x4(填充3字节)short c;     // 0x8
}; // 总大小:12字节(而非 16字节)​

3.this指针:

• Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和 Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了 ⼀个隐含的this指针解决这⾥的问题

• 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year, int month, int day)

• 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this- >_year = year;

• C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使⽤this指针

 另外需要注意一点,this指针其实存放在栈区,而不是对象里面

#include<iostream>
using namespace std;
class Date
{
public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){// this->_year = year;_year = year;this->_month = month;this->_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:// 这⾥只是声明,没有开空间 int _year;int _month;int _day;
};int main()
{Date A;return 0;
}//成员函数在传参时都有一个类的指针类型的this指针,这个this指针编译器不会显示出来,但实际上他是存在的,看上边这串代码,如果再函数调用赋值的时候,可以手动把this指针加上去,这样其实并不会报错。这就说明这个this指针是真实存在的

4.关于空指针访问成员变量的注意点:

先看一下下面这两个代码:

       这两串代码运行的结果并不相同,已知第一个是正常运行,第二个是运行崩溃,首先我们应该知道不管是C语言中还是C++中,解引用空指针并不会编译报错,只会运行崩溃

其次再来分析问什么第二个是运行崩溃

        首先成员函数不会占用物理内存,只有成员变量会,实例出nullptr说明没开空间,但仔细看第一个程序是不需要访问成员变量的,所以不开空间也没有报错,而第二个程序访问了开空间的成员变量:_a,所以运行崩溃了

三.类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数:

接下来我会对其中的几个做出详细介绍:

1.构造函数:

       构造函数也是一种成员函数但他和我们写的普通构造函数不同的是,他是在我们实例化类的对象是默认调用的,也就是说,实例化对象是他自己会去主动调用这个构造函数,其本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的 特点就完美的替代的了Init

接下来说说它的基本特点

函数名和类名相同;

没有返回值:

#include<iostream>
using namespace std;
class DATE
{
public:DATE(int year = 2000, int mouth = 11, int day = 1){_year = year;_mouth = mouth;_day = day;}
private://成员函数//private成员函数不能直接访问,可以通过成员函数访问int _year;int _mouth;int _day;
};
int main()
{DATE d1;return 0;
}//上面这串代码中定义了一个日期类,并实例化出一个对象d1,调试可以看到,实例化d1自动调用了DATE这个构造函数,给d1的三个成员变量进行了赋值//构造函数也有很多种,第一种无参构造函数。第二种是全缺省构造函数,第三种就是不写构造时编译器默认的构造函数(接下来我会具体说说这三种函数),如果我们在实例化的时候只写这个对象就像上面这串代码这样:DATE d1; 这样调用的构造函数叫默认构造

       

//无参构造函数
DATE()
{_year = 1;_mouth = 1;_day = 1;
}//全缺省构造函数
DATE(int year = 2000, int mouth = 11, int day = 1)
{_year = year;_mouth = mouth;_day = day;
}// 带参构造函数 
Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}

       除了以上几点还有一点需要额外注意:如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成,也就是说我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化,  如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表的问题,本文先放一下,下一篇文章再作详细介绍

       读到这里,会发现一个问题就是既然系统会自动生成默认构造函数,那为什么我们还需要自己去写构造函数?举个例子:

当类需要动态分配内存(如 new  或 malloc )时,默认构造函数无法自动释放资源,必须手动管理:

class Buffer {
public:int* data; // 动态内存// 自定义构造函数:初始化 dataBuffer(int size) : data(new int[size]) {std::cout << "Buffer initialized with size " << size << std::endl;}// 析构函数:释放资源~Buffer() {delete[] data;std::cout << "Buffer destroyed" << std::endl;}
};int main() {Buffer buf(1024); // 调用自定义构造函数return 0;
}

默认构造函数不会初始化 data ,导致未定义行为(如悬空指针),自定义构造函数确保 data 正确分配内存

2.析构函数:

       析构函数可以在类对象销毁时自动调用,释放我们的内存空间。就好比之前实现的栈这个数据结构,我们需要把我们malloc出来的空间都free掉,那么这个时候如果是使用c++里面的类来完成的话,在我们的栈销毁时(该对象生命周期结束时)就可以自动调用析构函数释放内存


~Stack()
{cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;
}

析构的特点也很明显:

1. 析构函数名是在类名前加上字符~

2.不需要写返回值

3.和构造函数一样,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数

4.一个类只有一个析构且当类成员不需要释放空间时,不需要自己写析构函数

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};

3.拷贝构造函数:

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数

#include<iostream>
using namespace std;class DATE
{
public:DATE(int year, int mouth, int day){_year = year;_mouth = mouth;_day = day;}void Print(){cout << _year << "年" << _mouth << "月" << _day << "日" << endl;}
private://成员函数//private成员函数不能直接访问,可以通过成员函数访问int _year;int _mouth;int _day;
};
int main()
{DATE d1(10,10,10);DATE d2(d1);//调用拷贝构造d1.Print();d2.Print();return 0;
}
//注意,第一个参数必须是引用。否则编译器会报错。为什么会报错呢?理解一下,如果说我们传入的第一个参数没有引用,那么这个形参是得拷贝一份我们的实参,怎么拷贝呢?他还是得调用我们的拷贝构造函数去拷贝,那这就形成了闭环,而这样无限拷贝下去编译器是不允许的//对于没有主动写拷贝构造的类,编译器也会默认生成一个拷贝构造,对于内置类型进行浅拷贝,也就是只拷贝值,对于自定义类型成员会调用他的拷贝构造。

但还有几点需要注意:

1.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造,但像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉),像MyQueue这样的类型内部主要是⾃定义类型 Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现 MyQueue的拷⻉构造(前提是Stack这个类有析构)

2.传值返回会产⽣⼀个临时对象调⽤拷⻉构造传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象在当前函数结束后还在,才能⽤引⽤返回

4.运算符重载:

运算符重载简而言之就是赋予我们常见的运算符以新的定义与使用场景,比如+号原来只可以用于数字之间的运算,但经过运算符重载之后,使其可以进行日期之间的计算,诸如此类:

bool operator==(DATE x)
{return _year == x._year && _mouth == x._mouth && _day == x._day;
}

以下是几个注意点:

1.不能对c++没有的符号进行重载

2、以下五个运算符不能进行重载:

.*      ::      sizeof       ? :       .

3.运算符重载的参数列表至少要含有一个自定义类型,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)

4.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分

四.日期类实现

//Date.h
#pragma once
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class DATE
{
public:DATE(int year = 2000, int mouth = 11, int day = 1){_year = year;_mouth = mouth;_day = day;}//短小多次调用函数使用inline//clase默认inlineint GetMouthDay(int year, int mouth){assert(mouth > 0 && mouth < 13);static int mouthDayArray[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };//多次访问直接定义静态数组if (mouth == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))//让容易不通过的条件放前面{return 29;}else return mouthDayArray[mouth];}DATE& operator+=(int day);//声明运算符重载DATE& operator+(int day);void Print(){cout << _year << "年" << _mouth << "月" << _day << "日" << endl;}//日期比较bool operator>(DATE x){if (_year > x._year) return true;else if (_year < x._year) return false;if (_mouth > x._mouth) return true;else if (_mouth < x._mouth) return false;if (_day > x._day) return true;else return false;}bool operator==(DATE x){return _year == x._year && _mouth == x._mouth && _day == x._day;}bool operator < (DATE x){return !(*this > x) && !(*this == x);}bool operator!=(DATE& d2){return !(*this == d2);}DATE operator++(int){//后置加加返回原值//注意这个临时变量出了这个函数就销毁了所以不能引用返回DATE tmp(*this);_day++;if (_day > GetMouthDay(_year, _mouth)){_day = 1; _mouth++;}if (_mouth > 12){_year++;_mouth = 1;}return tmp;}//两日期相减int operator-(DATE& d1);
private://成员函数//private成员函数不能直接访问,可以通过成员函数访问int _year;int _mouth;int _day;
};
//text.cpp#include"DATE.h"DATE& DATE::operator+=(int day)//表明所属类
{//由于更改了自身所以重载的是+=//引用返回可以避免拷贝,节省开销//不能返回空值,不然无法解决这种问题(a+=10)+=10//对于这种情况如果传dATE返回的话也无法改变原值,不符合预期。_day += day;while (_day > GetMouthDay(_year, _mouth)){_day -= GetMouthDay(_year, _mouth);++_mouth;if (_mouth == 13){_year++;_mouth = 1;}}return *this;
}
DATE& DATE::operator+(int day)
{//DATE tmp(*this);//拷贝//默认构造函数DATE tmp = *this;///同样也是调用默认构造函数tmp += day;return tmp;
}
int DATE::operator-(DATE& d1)
{int cnt = 0;int flag = 1;DATE max = *this;DATE min = d1;if (max < min){flag = -1;max = d1;min = *this;}while (max != min){cnt++;min++;}return cnt * flag;
}

以上就是关于日期类相关的函数代码了

欧克,时间也不晚了,就到这里吧

全文终


http://www.ppmy.cn/embedded/172744.html

相关文章

【redis】发布订阅

Redis的发布订阅&#xff08;Pub/Sub&#xff09;是一种基于消息多播的通信机制&#xff0c;它允许消息的**发布者&#xff08;Publisher&#xff09;向特定频道发送消息&#xff0c;而订阅者&#xff08;Subscriber&#xff09;**通过订阅频道或模式来接收消息。 其核心特点如…

DeepSeek进阶应用(一):结合Mermaid绘图(流程图、时序图、类图、状态图、甘特图、饼图)

&#x1f31f;前言: 在软件开发、项目管理和系统设计等领域&#xff0c;图表是表达复杂信息的有效工具。随着AI助手如DeepSeek的普及&#xff0c;我们现在可以更轻松地创建各种专业图表。 名人说&#xff1a;博观而约取&#xff0c;厚积而薄发。——苏轼《稼说送张琥》 创作者&…

探索DB-GPT:革新数据库交互的AI原生框架

引言 在AI与数据技术快速发展的今天,如何让自然语言与数据库实现无缝交互成为开发者关注的重点。DB-GPT作为一个开源的AI原生数据应用开发框架,通过整合大语言模型(LLM)、多代理协作、检索增强生成(RAG)等前沿技术,为开发者提供了高效构建数据驱动型AI应用的解决方案。…

PowerMock的使用

1. mock私有方法 待测试类 public class Demo {public void publicMethod() {System.out.println("public method invoke");protectedMethod("str");privateMethodA();privateMethodB();System.out.println("public method end");}protected v…

基于RTTR在C++中实现结构体数据的多层级动态读写

文章目录 1.背景2.RTTR2.1.注册结构体2.2.实现读操作2.3.实现写操作 3.读写调用例程4.结语 1.背景 目前有个项目&#xff0c;同一台电脑上的codesys程序将其结构体数据通过共享内存的方式写道了一个“共享内存”上。 我在取得内存数据后&#xff0c;需要对这个数据进行结构体的…

第13章贪心算法

贪心算法 局部最优求得总体最优 适用于桌上有6张纸币&#xff0c;面额为100 100 50 50 50 10&#xff0c;问怎么能拿走3张纸币&#xff0c;总面额最大&#xff1f;—拿单位价值最高的 只关注局部最优----关注拿一张的最大值拆解-----拿三次最大的纸币 不适用于桌面三件物品&am…

Linux常用命令速查手册

Linux常用命令速查手册 Linux常用命令速查手册1. 文件和目录操作1.1 查看当前目录&#xff08;pwd&#xff09;1.2 切换目录&#xff08;cd&#xff09;1.3 列出目录内容&#xff08;ls&#xff09;1.4 创建目录&#xff08;mkdir&#xff09;1.5 删除文件和目录&#xff08;rm…

xlua 运行原理

iOS限制App的二进制代码要一次性的包含在App内&#xff0c;也就是AOT&#xff0c;不支持JITLua代码作为资源文件&#xff0c;玩家下载&#xff0c;不涉及字节码&#xff0c;所以可以做热更Lua代码通过Lua虚拟机解释执行&#xff08;解释成机器码&#xff09;&#xff0c;并在虚…