【类与对象 -1】类的引入、访问及封装、定义、作用域、实例化、类大小的计算、this指针

1.类的引入

 C++兼容了C语言结构体的用法,但是同时又升级成了类。结构体中只能定义变量,类中不仅可以定义变量,还可以定义函数

例如,数据结构中实现栈,结构体stack中只定义了变量,要实现的函数在结构体外定义。以下代码就是对该实例的实现以及使用。

//C语言中结构体实现
struct stack
{
	int* a;
	int top;
	int capacity;
};
void StackInit(struct stack* s,int capacity = 4)
{
	int* tmp = (int*)malloc(sizeof(int) * capacity);
	if (tmp == nullptr)
	{
		perror("malloc fail");
		return;
	}
	s->a = tmp;
	s->capacity = capacity;
	s->top = 0;
}
void StackPush(struct stack* s, int x)
{
	//扩容
	s->a[s->top++] = x;
}
//C++中对结构体升级为类后实现
struct stack
{
	//成员变量
	int* a;
	int top;
	int capacity;
	//成员函数
	void StackInit(int icapacity = 4)
	{
		int* tmp = (int*)malloc(sizeof(int) * icapacity);
		if (tmp == nullptr)
		{
			perror("malloc fail");
			return;
		}
		a = tmp;
		capacity = icapacity;
		top = 0;
	}
	void StackPush(int x)
	{
		//扩容
		a[top++] = x;
	}
};
int main()
{
	struct stack s;
	StackInit(&s);
	StackPush(&s, 1);
    //类名可以代表类型
	stack1 s1;
	s1.StackInit();
	s1.StackPush(1);
}

C++中更喜欢用 class 来定义类。

2.类的访问限定符及封装

2.1访问限定符

请看下面的代码,思考结果是什么。

class Date
{
	int _year;
	int _month;
	int _day;
	void Init(int year, int month, int day)
	{
		_year = year;;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};

int main()
{
	Date s;
	s.Init(2003, 10, 1);
	s.Print();
	return 0;
}

是在屏幕上打印出 2003/10/1 吗?

不是的,反而会报错。显示函数 Init 和函数 Print 不可访问。这是为什么呢?

因为访问限定符的存在。

访问限定符也是C++实现封装的方式:用类将对象的属性与方法结合在一块,将对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

【访问限定符的说明】

①public修饰的成员在类外可以被直接访问;

②private和protected修饰的成员在类外不能直接被访问

③访问权限作用域从该访问限定符出现位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}即类结束。

class的默认访问权限为private,struct的默认访问权限是public(因为struct要兼容C)

要想让刚才的代码能够正确运行,应该对成员的访问权限进行设定。虽然class的默认访问权限是private,不过为了更清晰便将其标注出来。一般对于成员变量,只允许类内访问,将其设置为private。

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	void Init(int year, int month, int day)
	{
		_year = year;;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};

 2.2封装

面向对象的三大特性:封装、继承、多态

 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互

封装本质上是一种管理,让用户更方便使用类。

在C++中实现封装,可以通过类将数据及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接使用

3.类的定义

类定义的格式:

class  classname

{

    // 类体:由成员函数和成员变量组成

};  // 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面
号不能省略
类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者
成员函数

类有两种定义方式:

①声明和定义全部放在类体中。成员函数如果在类中定义,编译器可能会当将其作内联函数处理。这种方式是比较简单的。

②成员函数声明与定义分离。

这样实现的结果是不对的,报错显示未声明的标识符。因为在未指定作用域的情况下,搜索原则是先在局部区域查找,没找到再从全局域查找。而上面形式中 Init函数作用域内没有声明_year等变量,全局域中也未声明,所以会出错。

正确形式应是在定义时在成员函数前加类名::

4.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。

class Person
{
private:
	char _name[20];
	char _gender[3];
	int _age;
public:
	void PrintPersonInfo();
};
//指定函数PrintPersonInfo属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

5.类的实例化

类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。

一个类可以实例化出多个对象。

圈出的部分是变量的声明还是定义呢? 

声明。变量的定义和声明有一个区别,变量定义需要开辟空间。那么该部分应该怎样定义?

 

也不可以通过下面的方式对其进行访问,因为它只是对_yaer等的声明,通过类域Date::找到了_year的出处,但是并没有空间。只能通过定义的类对象去访问,如s._year++;

6.类对象模型

6.1如何计算类对象的大小

class Date
{
public:
	int _year;
	int _month;
	int _day;

	void Init(int year, int month, int day)
	{
		_year = year;;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};
int main()
{
	Date s;
	cout << sizeof(s) << endl;
	return 0;
}

结果是12,根据结果看好像是三个成员变量在内存中所占的字节数。可是类中既可以有成员变量,也可以有成员函数,那么一个类的对象中到底包含了什么?如何计算类的大小?

先看以下代码,看看两个_year是同一空间吗,两个Init函数地址一样吗?

int main()
{
	Date s1;
	Date s2;
	s1._year = 2000;
	s2._year = 2000;

	s1.Init(2003, 10, 1);
	s2.Init(2003, 10, 1);
	return 0;
}

 通过汇编语言可以看出,s1._year和s2._year占用了不同空间,而s1.Init和s2.Init是相同的地址。

6.2类对象存储方式的猜测

对象中包含类的各个成员

 这样设计是可以的,但是有缺陷。

缺陷:每个对象中的成员是不同的,但是调用的是同一份函数,按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同的代码保存多次,浪费空间。

只保存成员变量,成员函数存放在公共的代码段

显然在以上两种存储方式中,计算机是按照第二种来存储的。

结论:

一个类的大小,实际就是该类中“成员变量”之和,当然要注意内存对齐;

当类中只有成员函数时,类大小也是1,为了占位,标识对象实例化时,定义出来存在过;

注意空类的大小,空类大小并不是0,编译器给了空类一个字节来唯一标识这个类的对象。

6.3类大小计算规则

遵循结构体对齐规则

对齐规则:

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

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。

   //VS中默认的值为8 

   //Linux中没有对齐数

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

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

7.this指针

7.1引入 

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, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

以上定义了一个Date类。我们知道类对象实例化后成员变量在其空间内,成员函数在一片公共区域,所以函数体内没有对不同对象的区分,那么当函数Init被d1对象调用时,被调用函数如何知道应该设置为d1而不是d2呢?这就引入了this指针来解决这个问题。

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

7.2this指针详解

        所有的成员函数都会多一个指针,这个指针就是this指针。它是成员函数的第一个参数,可以想象成下面的形式

 与我们对栈进行初始化相同,函数传参时将定义的栈对象地址传过去,用指针接收。

所以this指针并不难理解,但它又有自己的一些特性:

①C++规定:把this指针叫做隐含的this指针,所有的成员函数第一个参数都是它,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递;

②this指针的类型:类类型* const ,即成员函数中,不能给this指针赋值;

③在形参和实参的位置,不能显示写出

④在函数内部可以使用

this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

面试中常见问题

1.this指针存在哪里?
是以下哪个选项呢
        a、堆        b、栈        c、静态区        d、常量区        e、对象内
首先能够排除的是e,this指针不可能存在对象内。this指针本质上是“成员函数”的形参 ,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针
通过malloc的变量在堆上,所以也排除了a;
const修饰的变量不是就一定在常量区,如下面代码

 i虽然被const修饰,但 i 和 j 地址连续,是局部变量,都放在栈上;p是指针,也在栈上,但指针p所指的字符串在常量区;排除d;

static和全局变量在静态区,排除c;
所以this指针存储在栈上!因为它是一个形参。函数调用会建立栈帧,局部变量存在在函数栈帧中,即函数形参在栈区。
注意有些编译器可能会采用寄存器存储,如VS。


2.this指针可以为空吗?
(1)看以下代码,它的编译运行结果是什么呢?
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

结果是C、正常运行,运行结果如图。

为什么是能正常运行的?p是空指针,对空指针解引用应该是会报错的呀,但是上面的操作却可以正常运行。
-> 不一定存在解引用,这取决于后面所跟的值在不在指针所指向的空间里面。p是指向对象的指针,Print是类的成员函数,我们在类对象存储模型中讲过计算机采用的存储方式是 只保存成员变量,成员函数存放在公共的代码段,即Print函数不在p所指的空间内,在编译时就确定了Print函数的地址,也就是不存在对空指针的解引用,所以 正常运行
(2)看以下代码,它的编译运行结果是什么呢?
A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:
    void PrintA() 
   {
        cout<<_a<<endl;
   }
private:
 int _a;
};
int main()
{
    A* p = nullptr;
    p->PrintA();
    return 0;
}

结果是B、运行崩溃出现了this指针为空的情况,如下图

p是定义的指向类A实例化后的空指针,p->PrintA()中传的参数为空指针,PrintA函数的形参this指针接收空值。
cout << _a <<endl; 相当于 cout << this->_a << endl; ,_a存在与this指针所指的空间, this->_a 发生了对空指针的解引用,产生了运行错误。
以上就是对类与对象的初步认识,新的知识我们下篇博客见~(感谢观看,有不对的地方麻烦指出)
  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

今天学习了吗•

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

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

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

打赏作者

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

抵扣说明:

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

余额充值