
文章目录
- [1. 类的基础知识](#1. 类的基础知识)
-
- [1.1 类的定义](#1.1 类的定义)
-
- 1.1.1class和struct的对比
- [1.1.2 类内定义函数是inline](#1.1.2 类内定义函数是inline)
- [1.1.3 为什么类内函数默认inline?](#1.1.3 为什么类内函数默认inline?)
- [1.2 访问限定符](#1.2 访问限定符)
-
- [1.2.1 为什么要有访问限定符?](#1.2.1 为什么要有访问限定符?)
-
- [1.2.1.1 为什么要有封装性?](#1.2.1.1 为什么要有封装性?)
- [1.2.2 访问限定符作用的范围](#1.2.2 访问限定符作用的范围)
- [1.2.3 访问限定符一般的使用情况](#1.2.3 访问限定符一般的使用情况)
- [1.2.4 class和struct在范围限定符上的区别](#1.2.4 class和struct在范围限定符上的区别)
- [1.3 类域](#1.3 类域)
- 2.类实例化对象
-
- [2.1 对象大小](#2.1 对象大小)
-
- [2.1.2 对象大小是否包含成员函数大小?](#2.1.2 对象大小是否包含成员函数大小?)
-
- [2.1.2.1 成员函数无法存储于对象中](#2.1.2.1 成员函数无法存储于对象中)
- [2.1.2.2 成员函数指针无需存储于对象中](#2.1.2.2 成员函数指针无需存储于对象中)
- [3. this指针](#3. this指针)
-
- [3.1 现象:多个对象如何区分调用?](#3.1 现象:多个对象如何区分调用?)
- [3.2 原理:this指针如何区分对象?](#3.2 原理:this指针如何区分对象?)
这里是think的博客
希望可以一起交流知识,一起think
今天我们来学习C++类和对象(一)
一起来think
1. 类的基础知识
1.1 类的定义
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Init(int a,int b)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
A a;
a.Init(1,2);
return 0;
}
class为定义类的关键词,A为类名,其中和结构体一样最后要加上一个分号。
1.1.1class和struct的对比
-
class其实和struct定义方式是比较像的,但是我们看到C语言中的结构体中是没有函数的,只有变量,类中不只有变量,还有函数,类中的变量又称为类的属性或成员变量,类中的函数又称为类的方法或成员函数。
-
同时我们看到在定义类的变量(这是好理解的说法,正确说法:实例化对象)的时候类的名字就可以直接定义对象了
A a;而语法上为了兼容C语言的使用习惯,class A a;语法也是支持的,但是A a;是C++中的习惯写法。 -
在C++中将struct由结构体升级成了类,但是仍然兼容C语言,即
struct Tree t1和Tree t1在C++中都是可以的。
1.1.2 类内定义函数是inline
C++基础知识中我们讲过inline函数,其中定义在类中的函数都是inline函数,当然因为inline只是建议,所以如果遇到代码很长的函数或者递归函数,编译器是不会按照内联走的。
1.1.3 为什么类内函数默认inline?
- 我们定义在类中的函数,大部分函数都是很短的,那么建立函数栈帧的消耗可能要大于执行该代码,比如下面,所以这种短小函数非常适合内联。
cpp
void Init(int a,int b)
{
_a = a;
_b = b;
}
-
- 类是一个类型,和C语言中的结构体一样,一般定义在头文件里。在C语言中,结构体定义在头文件里没有什么问题,因为结构体主要用于描述类型,不会产生函数定义。
- 但是C++中的类除了成员变量,还有成员函数。如果类中定义了成员函数,并且类定义在头文件中,那么头文件被多个源文件包含后,可能会导致同一个函数在多个翻译单元(源文件预处理后的叫法)中被定义,从而产生链接阶段的重复定义问题。
- 因此,C++规定:类内定义的成员函数默认是inline函数。
-
inline函数有两个特点:
-
程序员可以通过inline关键字向编译器提出内联展开的建议,但是是否进行内联展开由编译器根据函数大小、调用频率等实际情况决定,inline并不是强制要求编译器展开。
-
C++标准规定,同一个inline函数可以在多个翻译单元中存在相同定义,只要这些定义完全一致。
第二点又可以分为三种情况:
-
如果函数被内联展开,调用处会直接替换为处理过的函数代码,此时该调用处不再需要函数调用,也不需要依赖独立的函数实体和函数符号,也就一般在没有函数指针的情况下不会生成函数符号和函数体(一般编译器都是这么实现的,但是有些编译器有例外,可能没有函数指针也会生成函数符号和函数体),因此不会产生重复定义问题。
-
如果函数没有被展开,编译器可能需要生成独立的函数实体和函数符号。对于多个翻译单元中产生的相同inline函数实体,链接器会根据inline规则以及具体实现机制进行处理,例如符号合并、选择其中一个定义等,不会像普通函数一样产生重复定义错误。
-
如果代码中存在需要函数实体的情况,例如获取inline函数的地址, 此时即使函数调用可以被展开,编译器也必须保留函数实体,因为程序需要使用该函数的地址。多个翻译单元中产生的相同inline函数实体,同样由链接器按照inline规则进行处理。
-
cpp
inline int Add(int a,int b)
{
return a+b;
}
int (*p)(int,int) = Add;
因此,即使类定义在头文件中,并且被多个源文件包含,类内定义的成员函数也不会因为重复定义而导致链接错误。
- 总结:inline函数不仅仅用于优化函数调用,更重要的是它允许函数在多个翻译单元中拥有相同定义。
如果类的成员函数声明和定义分离:
cpp
// Date.h
class Date
{
public:
void Print();
};
cpp
// Date.cpp
void Date::Print()
{
}
此时函数定义只存在于一个源文件中,头文件中只有函数声明,因此不会产生重复定义问题,也不需要inline。
1.2 访问限定符
访问限定符有public(公有的),private(私有的),protected(保护的),其中在讲到继承之前,都认为第2个和第3个是一样,一般使用private。
- 值得注意的是,访问限定符限制的是类外部对成员的访问权限。在类内部,即使成员被声明为 private,也仍然可以直接访问和使用。
- 其中pubilc限定的成员可以被类外直接访问,private,protected限定的则不可以。
cpp
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
1.2.1 为什么要有访问限定符?
C语言中方法(函数)和属性(变量),是分离的,并且属性外部是可以随意修改的,而C++中三大特性之一是封装性,既然是封装了,那么可以控制是否要被外部修改,也就体现其封装性,访问限定符也就此出现。
1.2.1.1 为什么要有封装性?

1.2.2 访问限定符作用的范围
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到};即类结束。
比如上面代码中pubilc到private中间的Init函数是公有成员函数,而private到}:中间的_year,_month,_day是私有成员变量。
1.2.3 访问限定符一般的使用情况
成员变量一般设定为私有,要在外部使用的成员函数设定为公共,不在外部使用的成员函数设定为私有。
1.2.4 class和struct在范围限定符上的区别
class中如果不写访问限定符,默认都是被private限定,struct如果不写访问限定符,默认都是被public限定。(其实也符合C++兼容C语言的情况,因为C语言中struct中的内容可以认为是公共的)
1.3 类域
类域和命名空间域的作用类似,都属于作用域的一种。
如果通过域作用限定符 :: 明确指定到某个类中查找成员,那么编译器会直接在该类域中查找,而不会继续向外层作用域查找。
2.类实例化对象
类实例化对象就像根据设计图建造房子。
设计图 本身不能住人,因为它只是描述了房子的结构、功能和建造方式,并不是真实存在的房屋。但是设计图规定了房子应该有哪些部分、每个部分有什么作用,以及应该如何建造。
C++中的类 也是如此。类本身不是一个具体的对象,不占用对象存储空间,它只是描述了一类对象应该具有的成员变量和成员函数,相当于对象的设计规范。
当使用类实例化对象时,编译器才会根据类的定义,为对象分配实际的存储空间,并创建对应的成员变量 。
成员函数则通常不存储在每个对象中,而是由所有对象共享。
类 = 设计图(描述对象有什么、能做什么)
对象 = 根据设计图建造出来的实体(占用实际内存)
2.1 对象大小
相比于C语言中的结构体,C++中的类内容上面多了一个成员函数,其中对于成员变量来说,其实和C语言中的结构体一样都遵循内存对齐规则。
其中规则如下:
- 结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
VS 中默认的值为 8
Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处(即嵌套结构体的对齐数是自己的最大对齐数),结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
结构体变为类,成员变为成员变量即可从C语言中的结构体到C++中的类
2.1.2 对象大小是否包含成员函数大小?
2.1.2.1 成员函数无法存储于对象中
函数被编译以后会形成一段指令,这段指令是放在代码区 的,是无法存储在对象中的,那么还有一种可能就是在对象中存储函数指针。
2.1.2.2 成员函数指针无需存储于对象中
存储函数指针是可以的,但是却没有必要,因为函数指针都是一样的,如果每一个对象都存一个函数指针,会导致重复存储,浪费空间。
既然函数代码存在代码区,那么函数指针存储在哪里?
- 这里就要提到了,其实普通函数的函数指针是不需要存储的,原因是普通函数调用,在编译期间就已经形成了call 函数符号的这种形式,等到链接期间,查找各个文件的符号表,找到函数符号对应的地址就可以拷贝到call 后面。(这里的地址实际上是编译器通过地址总线获得的,所以实际上不用存储的)
- 特殊情况是后面讲到的多态中会有运行期间才确定调用哪个函数的情况,所以函数指针是要存储的。(当然不是直接存储在类中,而是利用虚函数表来存储,以后再说)
3. this指针
3.1 现象:多个对象如何区分调用?
当我们创建多个对象时,虽然它们调用的成员函数代码相同,但是每个对象拥有独立的数据成员,因此成员变量的值可能不同,函数调用时传入的参数也可能不同。那么,编译器是如何区分当前调用成员函数的是对象1还是对象2,并让函数操作对应对象的数据呢?
3.2 原理:this指针如何区分对象?
cpp
void Init(int year,int month, int day);
//void Init(Date* const this, int year,int month, int day);
cpp
Date d1;
d1.Init(2024, 3, 31);
// d1.Init(&d1, 2024, 3, 31);
-
可以看到第1个是我们写在代码中的,而第二个注释中的是实际上编译器处理过的,即实际上我们写的代码,编译器会自己补上一个指针,即调用对应函数时会传递调用对象的地址,这个就是this指针。(其中可以看到this指针指向的内容是可以改变的,但是this指针的指向不可以改变)
-
this 指针的存在减少了代码冗余,使成员函数的调用更加简洁,相比于C语言的自己动手来说,C++将许多可以由编译器做的事情交给了编译器,也确实更加方便。
- 在调用成员函数时,this 指针由编译器自动传递,用户无需手动传递,并且也不能显式写出 this,否则会产生语法错误。
- 而在成员函数内部,访问成员变量时编译器会默认添加
this->,因此大部分情况下也无需手动使用 this,但是是无需不是不可以,想加语法上也没有问题。(其中补充一点,如果没有写this指针又有成员变量,编译器会由内到外查找,不是直接查找成员变量声明)
cpp
#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)
{
// 编译报错:error C2106: "=": 左操作数必须为左值
// this = nullptr;
// 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类实例化出对象d1和d2
Date d1;
Date d2;
// d1.Init(&d1, 2024, 3, 31);
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
- 但在一些特殊情况下,例如成员变量与形参同名
this->month = month、需要返回当前对象return *this;等场景,就需要显式使用 this。
问题:编译器是如何区分当前调用成员函数的是对象1还是对象2,并让函数操作对应对象的数据呢?
答案:传递对象地址,即可区分,这个对象地址又称this指针,用this->操作对应对象的数据。