【C++】类和对象(上篇)——类的定义,访问限定符与this指针

前言

        C 语言和 C++ 最大的区别就是一个面向过程,一个面向对象。而提到面向对象就不得部提到类,这一篇文章,我们主要探讨一下 C++ 中类的定义以及一些基本的权限。



目录

一、类的引入

二、类的定义

三、访问限定符

3.1 public

3.2 private / protected

四、封装

五、类的大小计算

5.1 类的存储方式

5.2 类的大小的计算方式 

5.3 结构体对齐规则

六、this 指针

6.1 this 指针的引入

6.2 this 指针特性

6.3 this 指针为空的情况


一、类的引入

C 语言结构体中只能定义变量,而在 C++ 中,结构体兼容了之前的 C ,不仅可以定义变量,也可以定义函数。而为了和 C 语言区分开来,C++ 中更喜欢使用 class 来代替 struct

例如:

#include < iostream>
using namespace std;

struct Person
{
	int age;
	void print()
	{
		cout << age << endl;
	}
	Person* next;
};

int main()
{
	Person x;
	x.age = 2;
	x.print();
}

同时,由于 C++ 中结构体直接作为类名,可以在结构体内直接定义 Person* next,而在 C 语言中则需要用 struct Person* next。

 二、类的定义

例如:

class Person
{
    int age;
};

class为定义类的关键字,其后跟着的 Person 就是类的名字,{} 中的为类的主体,注意类定义结束时后面分号不能省略

类的主体中内容称为类的成员:

主体中的变量称为 “ 类的属性 ” 或 “ 成员变量 ” ;

主体中的函数称为 “ 类的方法 ” 或 “ 成员函数 ” 。

类通常有两种定义方式:

1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理

例如:

#include
using namespace std;

class person
{
public:
	int age;
	void add()
	{
		age *= 2;
	}
};
int main()
{
	person x;
	x.age = 10;
	x.add();
	cout << x.age;
	return 0;
}

需要注意的是,这种情况下,编译器有概率将函数编译为内联函数,如下:

2. 类声明放在头文件中,成员函数定义放在源文件中,注意:成员函数名前需要加类名 ---  “ :: ”

例如:

#include
using namespace std;

class person
{
public:
	int age;
	void add();
};

void person::add()
{
	age *= 2;
}

int main()
{
	person x;
	x.age = 10;
	x.add();
	cout << x.age;
	return 0;
}

在类外定义的时候需要注意,应该在函数名前加上 类名 + ::

通常情况下练习可采用第一种,但是更建议采用第二种方式,将类的声明放在头文件中,函数单独放在一个源文件中。

三、访问限定符

【C++】类和对象(上篇)——类的定义,访问限定符与this指针_第1张图片

3.1 public

公有的意思即是所有人都可以使用,都可以访问,通常是类的函数的定义。

例如:

#include
using namespace std;

class person
{
public:
	int age;
	void add()
	{
		age *= 2;
	}
};
int main()
{
	person x;
	x.age = 10;
	x.add();
	cout << x.age;
	return 0;
}

对于变量 age 以及函数 add,我们都可以在类外访问到,而对比下面的 privavte 就能理解什么叫做 “ 公有 ”。 

3.2 private / protected

在初学阶段,private 和 protected 用法基本相同,因此在此不展开。

若 class 类内没有访问限定符,默认为 private 类型,而 struct 类内默认为 public 类型

私有是指在类外无法访问到 private 修饰的类的成员,通常是变量的定义,直接修改变量的函数的定义等。

例如:

#include
using namespace std;

class person
{
private:
	int age;
public:
	void get()
	{
		age = 2;
	}
	void print()
	{
		cout << age;
	}
};

int main()
{
	person x;
	// x.age = 1;
	x.get();
	// cout << x.age;
	x.print();
	return 0;
}

上图中注释行试图修改或者读取 age 的时候,就是对 private 修饰变量的一种访问,这种访问在类外无法进行,只能由类内的函数访问,如 x.print() 就可以访问到 age。

四、封装

我们都知道面向对象有三大特性:封装、继承、多态。今天我们主要谈谈封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。

通俗来说,封装就是让用户更加规范地使用类,使类内数据更加安全。

例如,我们定义了一个类,其中所有的变量都是 private 限定的,只能通过公有的函数来对变量进行访问,这就是我们对这个类进行封装的实例。

五、类的大小计算

5.1 类的存储方式

对于一个类而言,这个类的函数都是相同的,那么存储时还有必要给每一个类都创建一个新的函数吗?答案当然是否定的。这些类共用同样的一部分函数即可,有人也许就会想到指针,每个类不用创建新的函数,只需要保存到每一个函数的指针即可。

但是实际上 C++ 使用的类的存储方式是,仅保存成员变量的地址,将类成员函数放在另一片公共的区域,需要使用时直接在对应的区域内找即可。

【C++】类和对象(上篇)——类的定义,访问限定符与this指针_第2张图片

5.2 类的大小的计算方式 

 在计算成员变量的内存空间时,仍旧遵循 C 语言的结构体对齐规则

例如:

#include
using namespace std;

class A1
{
private:
	char c;
	int  x;
	void show();
public:
	void print();
	void add();
};

class A2
{
private:
	void show();
public:
	void print();
	void add();
};

class A3
{ };

int main()
{
	cout << sizeof A1 << ' ' << sizeof A2 << ' ' << sizeof A3 << endl;
	return 0;
}

 对于 A1 的内存就是遵循 C 语言的结构体对齐规则,对于 A2 和 A3 这类成员变量占内存大小为空的类而言,类的大小为 1,表示该类存在,所占 1 字节并不存储有效数据。

5.3 结构体对齐规则

1. 第一个成员在与结构体偏移量为0的地址处。

2. 其他成员变量要对齐到对齐数的整数倍的地址处。 注意:对齐数 = 编译器默认对齐数与该成员变量大小的较小值。VS2022中默认的对齐数为8。

3. 结构体总大小为:对齐过程中的最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

六、this 指针

6.1 this 指针的引入

在上面我们已经知道,对于同一个类型的类而言,成员函数都是在同一块空间,那么编译器是如何识别是哪一个类在调用函数呢?

我们先来看看下面的代码:

#include
using namespace std;

class student
{
private:
	string _id;
	string _name;
	int _age;
public:
	void Inset(string id, string name, int age)
	{
		_id = id, _name = name, _age = age;
	}
	void print()
	{
		cout << _id << endl << _name << endl << _age << endl;
	}
};

int main()
{
	student a;
	student b;
	a.Inset("001", "大黄", 19);
	b.Inset("002", "小黄", 18);
	a.print();
	b.print();
	return 0;
}

我们可以看到当 studen 类的 a, b 分别调用 print 函数的时候,打印出来的结果并不相同,可是我们并没有任何参数传入,print 函数又是怎么知道是哪一个类在进行调用的呢?答案就是 this 指针,print 函数实际上隐含了一个 this 指针传参,如下:

void print(student* const _this)
{
	cout << _this->_id << endl << _this->_name << endl << _this->_age << endl;
}

上图只是一个类似的说法,实际并非完全一样!上图中的 this 指针就是指向当前调用函数的类的一个指针,通过这个指针,函数才能知道应该访问哪一个类的成员变量。需要注意的是,this 指针的定义和传递都是编译器自主实现,用户无法代替编译器定义、传参,但是我们可以对 this 进行使用

6.2 this 指针特性

1. this 指针的类型:类类型* c onst,即成员函数中,不能修改 this 指针本身的指向的地址,即不能给 this 指针赋值,但可修改其所指向的地址存储的内容。

2. this 指针只能在 “ 成员函数 ” 的内部使用。

3. this 指针本质上是 “ 成员函数 ” 的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针,this 指针存储在栈帧之中,部分编译器会进行优化,存储在寄存器之中。

6.3 this 指针为空的情况

我们先来看看下面两个程序:

#include
using namespace std;

class student
{
private:
	string _id;
	string _name;
	int _age;
public:
	void print()
	{
		cout << _id << endl << _name << endl << _age << endl;
	}
};

int main()
{
	student* a = nullptr;
	a->print();
	return 0;
}
#include
using namespace std;

class student
{
private:
	string _id;
	string _name;
	int _age;
public:
	void print()
	{
		cout << "hello world ! " << endl;
	}
};

int main()
{
	student* a = nullptr;
	a->print();
	return 0;
}

大家可以先自己判断一下每一个程序的运行结果。

答案是:第一个程序会运行崩溃,第二个程序则正常运行。

也许大家会觉得奇怪,a 不是空指针吗?为什么还可以写 a->print(),这样不就是对空指针进行访问了吗?

实际上不是,我们已经知道类的成员函数实际上不在类内,而是在一片公共区域,因此 a->print() 本质上没有发生解引用,只是将 a 的地址传参到了 this 指针之中。对于第二个程序,函数调用时,没有对 this 指针进行解引用,因此正常运行,但是第一个程序打印成员变量时,是通过对 this 指针的解应用来进行访问的,因此发生了程序崩溃。

欢迎来和小黄一起学习呀~

你可能感兴趣的:(C++,学习经历,c++,c语言,经验分享)