超详解C++类与对象(上)

目录

 1.面向过程与面向对象

2. 类的引入

2.1. 类的两种定义

2.1.1. 第一种

2.1.2. 第二种

2.2. 单文件与多文件书写 

2.3. 成员变量的命名

3. 类的访问限定符与封装

3.2. 封装

4. 类对象模型

4.1. 类对象的实例化

4.2. 类对象的存储

4.2.1. 存储一

4.2.2. 存储二

4.2.3. 存储三 

4.3. 类对象的大小

4.3.1. 一般类的计算

4.3.2. 空类的计算

5. this指针

5.1. this指针的引出

5.2. this指针的特点

5.3. 两道问题

5.3.1. 问题一

5.3.2. 问题二


 💓 博客主页:C-SDN花园GGbond

⏩ 文章专栏:玩转c++

 1.面向过程与面向对象

我们之前学习的C语言就是一种面向过程的语言,它强调事件的具体实现过程,一般以函数来具体实现。比如说我们用面向过程的思想就洗衣服可以分为以下几个步骤:

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成

2. 类的引入

在C++中,在原来C语言结构体的基础上引入了类的概念。与C语言最大的不同就是,C++可以在类中定义函数。

而由类声明定义的变量,我们称为对象

2.1. 类的两种定义
2.1.1. 第一种
#include<iostream>
using namespace std;
struct Date
{
	int a;
	int b;
};
int main()
{
	Date d1;//ok
	struct Date d2;//ok
	return 0;
}

因为C++兼容C语言,所以可以利用C语言的结构体关键字struct来定义类。但是由于struct在C++中升级为类,所以在定义对象时并不需要写struct关键字。

2.1.2. 第二种

第二种是由C++的关键字class构成,其语法结构如下:

class className
{
// 类体:由成员函数和成员变量组成
};

#include<iostream>
using namespace std;
class Date
{
	int a;
	int b;
};
2.2. 单文件与多文件书写 

在定义类时,我们可以将类的定义与声明在同一文件书写,或者在不同文件书写。

  1. 声明和定全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
  2. #include<iostream>
    using namespace std;
    class Date
    {
        //定义与声明一起
    	int Add(int x, int y)
    	{
    		return x + y;
    	}
    	int year;
    	int month;
    	int day;
    };
    

  1. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::。
#include<iostream>
using namespace std;
//test.h
class Date
{
    //定义与声明一起
	int Add(int x, int y);
	int year;
	int month;
	int day;
};
//test.cpp
int Date::Add(int x,int y)
{
    return x+y;
}

 一般在我们平时练习时,更常用将定义与声明放在类中。但是在实际工程中,更倾向于奖定义与声明分类。

2.3. 成员变量的命名

 虽然C++标准并没有规定成员变量的命名规则,但是大家约定俗成地在定义变量时会有一套特定的规则,目的就是解决可能存在的命名冲突的问题,比如说下面这段代码:

class Date
{
	void Init(int year, int month, int day)
	{
		//命名冲突
		year = year;
		month = month;
		day = day;
	}
	int year;
	int month;
	int day;
};

这时为了解决问题这类问题,我们在定义成员变量时会对其进行特定地修饰。比如说_变量名或者是m_变量名或者变量名_。不同公司,不同的程序员可能都有一套自己的命名规则,但主流一般就是以上三种。 

class Date
{
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

3. 类的访问限定符与封装

3.1. 访问限定符
在C++类中有三种访问限定符:**public,private,protected。**他们每一个都有自己独特的作用:

public修饰的成员在类外可以直接被访问。
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。
class的默认访问权限为private,struct为public(因为struct要兼容C)。

class Date
{
//可以被直接访问
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//不能被直接访问
private:
	int _year;
	int _month;
	int _day;
};
3.2. 封装

封装:用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

4. 类对象模型

4.1. 类对象的实例化

在类中的成员变量实际是一种声明,相当于一个设计图纸。而我们利用类名定义的对象就是类对象的实例化,相当于通过设计图纸实际创建出来。单独的类是并不占据实际空间的大小。

4.2. 类对象的存储

我们知道了类对象的创建,那么具体类中的成员变量与成员函数又是如何存储的呢?

4.2.1. 存储一

每次创建对象时,都开辟一个空间存储类成员变量与成员函数。

4.2.2. 存储二

每次创建对象时,都开辟一个空间存储类成员变量与成员函数的地址。

4.2.3. 存储三 

每次创建对象时,都开辟一个空间存储类成员变量。而成员函数提前单独存储一个区域

首先如果是存储一的话,每次对象的实例化都会开辟函数的空间。而每个函数的功能都是一样的,这就造成了空间的浪费。而如何判断时存储二还是存储三,我们可以通过计算类大小来判断。 

4.3. 类对象的大小

4.3.1. 一般类的计算


类型对象的大小我们可以借助运算符sizeof计算,并且类的大小也遵循结构体内存对齐规则:

结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。(VS 中默认的值为 8 ,Linux中gcc没有默认对齐数,对⻬数就是成员⾃⾝的⼤⼩)
结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

#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;
	cout << sizeof(d) << endl;
	return 0;
}

根据内存对齐规则,我们知道成员变量的大小就是12,所以证明成员函数是存在于内存的其他位置。所以说存储三正确。一般这个存储成员函数的区域我们称之为公共代码段 

4.3.2. 空类的计算

当类中只有成员函数,或者什么都没有时。类的大小又为多少呢

// 类中仅有成员函数
class A1
{
public:
	void func2() {}
};
// 类中什么都没有---空类
class A2
{
};
int main()
{
	A1 d1;
	A2 d2;
	cout << sizeof(d1) << endl;
	cout << sizeof(d2) << endl;
	return 0;
}

为什么空类的大小为1,而不是0呢?其实并不能难像,因为在我们进行空类的实例化时必须要有空间存储对象的大小。这是编译器就会默认给一个字节大小来标记这个类的对象,实际操作中实用性也很少。 

5. this指针

5.1. this指针的引出

我们首先来看这一段代码

#include<iostream>
using namespace std;
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 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

在C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数。通过不同的对象地址来分辨不同的对象,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。实际代码如下:

void Init(Date* this, int year, int month, int day)
{
	this->_year = year;
	this->_month = month;
	this->_day = day;
}
d1.Init(&d1,2022, 1, 11);//实际传参

但是注意:我们并不能将隐式传参书写出来,因为这是编译器默认添加的。 

5.2. this指针的特点


this指针的类型:类型为*** const**,即成员函数中,不能给this指针赋值。
只能在“成员函数”的内部使用。
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

5.3. 两道问题
5.3.1. 问题一
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	Betty* p = nullptr;
	p->Print();
	(*p).Print();
	return 0;
}

为什么程序会正常运行,对空指针解引用不是会发生运行崩溃吗?首先我们得明白成员函数并不存放在类对象中,而是存放在公共代码段。虽然我们表面看上去解引用,但实际上编译器不需要通过解引用去找对应函数,只需要去公共代码区执行对应函数即可。 

5.3.2. 问题二
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	Betty* p = nullptr;
	p->Print();
	(*p).Print();
	return 0;
}

 这里就引起程序崩溃,因为我们知道访问对应的成员变量,会传递对应对象的地址。而这里的地址为nullptr,通过nullptr->_a引起程序崩溃。


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值