类和对象 [类的定义、作用域、实例化、以及对象大小计算、this指针]

在这里插入图片描述

首先类和对象今天初步认识一下

面向对象和面向过程的区别

首先我们知道C语言是一们面向过程的语言,关注的是函数执行的过程,数据和方法是分离的。
C++是一门面向对象的语言,主要关注对象,将一件事情差分成不同的对象,靠对象间的交互来完成。

类的定义

关于类的定义这里因为C++是兼容C语言的,所以C++给C语言的struct一个新的定义,就是用来定义类,当然C语言中关于结构体的用法c++这里同样也是兼容的。

struct name
{
	void Init()
	{
		;
	}
	void Push()
	{
		;
	}

	int* _a;
	int _top;
	int _capacity;
};

这里我们就使用struct定义了一个类,一个类实际就是一个类型(花括号括起来的就是一个作用域,类的作用域就叫类域),可以看到在类里面,我们可以定义成员变量_top等,也可以定义成员函数就是Init和Push。name就是类名,成员变量和成员函数组成了类体。

在这里插入图片描述

通过上面这段图片上的代码,我们可以看到,我们用定义好的类创建了一个对象s,然后使用类里面的成员函数进行了初始化。这就是类的使用。

但是struct毕竟是C语言的产物,C++还有一个关键字class来定义类,上面这段代码我们将struct换成class也是完全没有问题的。

如果我们将struct改成class之后会发现,报错了,其实我们应该在前面加上一个public:原理就在下面

class stu
{
public:
	void Init(const char*arr,const char*id,const char*sex)
	{
		strcpy(_name, arr);
		strcpy(_id, id);
		strcpy(_sex, sex);

	}

	char _name[20];
	char _id[15];
	char _sex[5];
};


int main()
{
	struct stu s;
	s.Init("zhangsan", "0007", "nan");
	return 0;
}

一般来讲更推荐将上述代码分文件写,因为类里面定义函数这个函数可能会被编译器当成内联函数。
所以我们可以分文间如下所示写

//main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
int main()
{
	class stu s;
	s.Init("zhangsan", "0007", "nan");
	return 0;
}
//stu.h
#pragma once

#include<iostream>
#include<cstring>
using namespace std;

class stu
{
public:
	void Init(const char* arr, const char* id, const char* sex);

	char _name[30];
	char _id[20];
	char _sex[10];
};
//stu.c
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
void stu::Init(const char* arr, const char* id, const char* sex)
{
	strcpy(_name, arr);
	strcpy(_id, id);
	strcpy(_sex, sex);
}

这里要记住一个点,就是定义类的时候如果用的class那么实例化的时候也应该用class,如果用struct就会报警告。

类的访问限定符和封装

上面代码中的public是什么,就是这里要说的类的访问限定符。

类的访问限定符一共有三个:public,private,protected在此处我们暂且认为private和protected是类似的。其中public的作用就是字母意思公有,也就是类里面的对象在public的范围之下(从public出现的地方到下一个访问限定符出现或者到类结束,这段区间都是public的作用)的成员函数或者成员变量是可以在类外被直接访问到的。比如上面代码中的成员函数Init或者成员变量id_[20],可以直接在外面被修改。但是被private或者protected的范围内的成员就不可以在外面被直接访问。

//main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
int main()
{
	class stu s;
	s.Init("zhangsan", "0007", "nan");
	return 0;
}
//stu.h
#pragma once

#include<iostream>
#include<cstring>
using namespace std;

class stu
{
public:
	void Init(const char* arr, const char* id, const char* sex);
private:
	char _name[30];
	char _id[20];
	char _sex[10];
};
//stu.c
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
void stu::Init(const char* arr, const char* id, const char* sex)
{
	strcpy(_name, arr);
	strcpy(_id, id);
	strcpy(_sex, sex);
}

可以看到我在stu类里面加了一个private,这样就让外面不能直接访问到成员对象,只能通过成员函数来修改成员对象,这样的好处就是,用户的行为都在掌控之中,不会出现,直接修改成员对象导致某些错误的情况发生。

这里还要提一下,那就是如果我们不加类访问限定符,那么struct默认为public(因为struct要兼容C语言的结构体所以应该是能够随意访问的),class默认为private。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
**【面试题】 **
**问题:C++中struct和class的区别是什么? **
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。 和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。

封装

封装实际上是将数据与操作数据的方法结合起来,然后隐藏对象的部分属性和实现细节(一般成员变量都是隐藏的)对外只公开一些结构,通过这些接口来进行对象间的交互。

封装可以使得对象更加安全,不存在说用户直接去修改对象属性的这种危险行为,用户只能通过调用函数接口来访问对象,行为更加可控。

所以一般上,我们想给外部展示的定义为public,不想给外部展示的定义为private或protected。

类的作用域

我们定义了一个类,这个类里面就是这个类的作用域,又叫做类域。所有成员都在类体内,如果想要在类体外定义类成员,就要用到类作用域解析符。

class stack
{
public:
    void Init();
    
    int size;
    int capacity;
    int* a;
};
void stack::Init()
{
    //.....
}

在类外定义类成员就要加这个,来表示这个成员是属于哪一个类的。

类的实例化

当我们定义完了一个类,我们用这个类创建出来了对象,这个就叫做类的实例化。

class stack
{
public:
    void Init()
    {
        ;
    }
    void Push()
    {
        ;
    }
private:
    int*_a;
    int _size;
    int _capacity;
};

int main()
{
    stack s1;
    s1.Init();
    s1.Push();
    return 0;
}

上面我们用stack定义出来了一个对象s1,这个过程就是类的实例化。
当我们仅仅定义出来一个类这时候是不占用内存的就是创建了一个类型,这相当于是一个蓝图,通过这个蓝图可以创建对象,当对象创建出来之后才会在内存中占用空间。

类对象大小的计算

这里要注意当我们计算s1的大小的时候,可以发现这里计算出来的大小是12,那么问题来了,成员函数不占内存吗?
在这里插入图片描述

当然不是啦,只是我们实例化出来的对象不包含成员函数,我们定义类的成员函数都存放在公共的代码段并不是存放在对象中,不然如果创建多个对象是不是会有很多个成员函数呢?这样就不对了,应该是多个成员变量都调用的是同一个成员函数。

class stack
{
public:
    void Init()
    {
        ;
    }
    void Push()
    {
        ;
    }
private:
};

int main()
{
    stack s1;
    cout << sizeof(s1) << endl;
    return 0;
}

再来看这段代码,如果我们不给他定义成员变量那么计算的大小会是多少呢?会不会是0?

在这里插入图片描述

答案是:不会的,可以这样想一想,如果这个对象的大小是0,那么我定义很多个这样的对象,他们应该怎么区分呢?如果大小是0,他们就没有地址也就无法区分开来了。所以不管是空类(什么成员都没有)或者是没有成员对象的类,他们的大小都是1,这个一个字节的意义就是为了占位,占了一个地址,也可以区分出来不同的对象 。

这里可以还有同学对上面的12的计算有疑惑,这里说明,类的对象的大小计算是根结构体哪里一样的,同样也是符合内存对齐的。

总结,再来看下面这段代码中的类的大小分别是多少呢?

// 类中既有成员变量,又有成员函数
class A1 {
public:
 void f1(){}
private:
 int _a;
};
// 类中仅有成员函数
class A2 {
public:
 void f2() {}
};
// 类中什么都没有---空类
class A3
{};

答案:A1创建出来的对象大小是4,A2和A3都是1,所以现在我们也可看出来,对象中不包含成员函数。

关于结构体内存对齐和大小计算

结构体内存对齐规则
1,第一个成员在地址偏移量为0的地址处。
2,每个成员的第一个字节存放的位置的地址偏移量必须是对齐数整数倍(如果编译器默认对齐数是8,int就是4,同时对齐数取两个对齐数中最小的也是4,double是8,对齐数也是8)
3,结构体整体的大小必须是,最大对齐数的整数倍(VS中默认为8,Linux中的默认值为4,默认参数一般设置成1,2,4,8,16.我们可以通过#pragme pack (4),将编译器的默认对齐数修改为4.)
最大对齐数就是变量类型中最大的那个和默认对齐数相比较取较小的那个就是最大对齐数啦
(如果要恢复编译器默认对齐数,用#pragma pack()即可)
4,如果结构体内嵌套了其他结构体变量,那么这个结构体变量的大小结算也和上面相同,这个变量的对齐数就是它这个结构体的最大对齐数。

#pragma pack (8)

struct test2
{
    char c;
    double a;
};

int main()
{
    stack s1;
    cout << sizeof(struct test2) << endl;
    return 0;
}

这段程序可以很好的说明最大对齐数,当默认对齐数为1的时候,这大小是9
因为默认对齐数是1,和变量类型中最大的8取较小值就是1.
当默认对齐数为4的时候这个结构体大小为12,8和4取较小的取到了4.
当默认对齐数是8,就没区别了,默认和最大都是8,那结果是16.

**【面试题】 **

  1. 结构体怎么对齐? 为什么要进行内存对齐
  2. 如何让结构体按照指定的对齐参数进行对齐
  3. 如何知道结构体中某个成员相对于结构体起始位置的偏移量
  4. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
    1,为什么要进行内存对齐,这是一种用空间换时间的做法,比如32位机有32根地址线,这样cpu每次读取都可以读取到四个字节,如果结构体都堆在一起放置,那么cpu每次读到的数据可能并不是只有一个数据,还可能截断读取了其他数字的部分数据,下次读的时候还需要将数据连接起来,效率就会很慢,若内存是对齐的,每次读取的数据都是一个数字的数据,就不需要cpu再去处理数据的问题了。
    百度百科对此解释:

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

3,如何知道结构体成员的偏移量呢?

使用函数offsetof,引用的头文件是

#pragma pack (8)
#include<cstddef>
struct test2
{
    char c;
    double a;
};

int main()
{
    stack s1;
    //cout << sizeof(struct test2) << endl;
    cout << offsetof(struct test2, a)<<endl;
    return 0;
}

this 指针

关于什么是this指针,我们来看一段代码

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;
	d1.Init(2022, 6, 17);
	d1.Print();
	return 0;
}

这段代码我们定义了一个日期类,然后初始化,这里要注意一点,为了放置初始化函数这里的形参和类的成员函数名重合我们一般在成员函数名之前加上一个下划线,以此来区分。

下面我们来思考问题,当我们用不同的对象都调用Print函数和Init函数的时候,这个函数是如何区分我们的形参要赋值给哪一个对象的成员变量呢?

这就引出来了我们要说的this指针

//其实我们在调用Init的时候看似是这样传参
d1.Init(2022,6,17);
//实际上是
d1.Init(&d1,2022,6,17);

d1.Print();
d1.Print(&d1);

没错我们在传参的时候实际上还将对象的地址传了过去,只是这些是编译器自己处理的我们看不到而已。下面再来看函数部分是如何实现的。

//实际上的Init是这样的
	void Init(Date*this,int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
//Print也是如此
	void Print(Date*this)
	{
		cout << this->_year << " " << this->_month << " " << this->_day << endl;
	}

就像是C语言中的结构体指针,通过箭头来访问结构体内的对象,这个this指针就是同这种方式访问到每个对象的成员变量。
但是参数上的Data*this是编译器隐含加上的,我们不可以手动加上,否则在传参的时候就会出现对应不上的问题。但是我们是可以直接在这个成员函数内使用this指针的,比如下面这样:

	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

总结:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

this指针的特性:

1,this指针的类型是:Date*const this,因为this指针是不可修改的所以用const保护起来。
2,this指针只能在成员函数中使用。
3,this指针是一个成员函数的形参,是对象在调用函数的时候将对象的地址当作实参传递的。所以this指针并不在对象中,而是在存在栈区里面。
4,this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户
传递。

// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}

	void Show()
	{
		cout << "Show()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = NULL;
	p->PrintA();
	p->Show();
}

首先这段代码可以编译通过,但是执行的时候会挂掉,第一p->PrintA的时候并没有对空指针进行解引用,虽然这类的指针式空指针,但是调用PrintA函数的时候只是把p当作参数传给了隐含的this指针,因为PrintA并不在对象中,所以也不需要对p解引用到对象里面找这个函数,之需要区公共代码段内找这个函数即可,因此是可以编译通过的。
程序挂掉的原因是在PrintA中访问_a,实际上是this->_a.这里对空指针进行了解引用,所以程序崩溃了,如果屏蔽掉PrintA则程序没有问题。
注意:PrintA和Show函数的地址并没有存在对象里面。这些成员函数都是存在公共代码段的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KissKernel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值