【C++精华铺】4.C++类和对象(上)面向对象、类、this指针

目录

[1. 面向过程和面向对象](#1. 面向过程和面向对象)

[2. 类的引入](#2. 类的引入)

[3. 类的定义](#3. 类的定义)

[4. 类的访问限定符和封装](#4. 类的访问限定符和封装)

[4.1 类的访问限定符](#4.1 类的访问限定符)

[4.2 封装](#4.2 封装)

[5. 类的作用域](#5. 类的作用域)

[6. 类的实例化](#6. 类的实例化)

[7. 类对象模型](#7. 类对象模型)

[7.1 类对象的存储方式](#7.1 类对象的存储方式)

[7.2 类的大小](#7.2 类的大小)

[7.2.1 空类的大小](#7.2.1 空类的大小)

[7.2.2 结构体内存对齐规则](#7.2.2 结构体内存对齐规则)

[8. this关键字深入讲解](#8. this关键字深入讲解)

[8.1 this指针的引出](#8.1 this指针的引出)

[8.2 this指针的特性](#8.2 this指针的特性)

9.空指针调用成员函数的问题


1. 面向过程和面向对象

C语言是一门面向过程的语言,面向的解决问题的过程,通过函数调用来依次解决问题:

譬如洗衣服:拿盆-放水-放衣服-放洗衣粉... ...

而C++是基于面向对象的,关注的是对象,同样拿洗衣服举例C++关注的是:人、衣服、洗衣粉... ...

2. 类的引入

在C语言中结构体只能定义变量,在C++中结构体被升级成了类,里面不仅可以定义变量,也可以定义函数。

cpp 复制代码
struct Date
{
	void addDate(int x) {}
	int _year;
	int _month;
	int _day;
};

但是上面的结构体在C++中更愿意用class来定义。

3. 类的定义

class为定义类的关键字,{}中的内容为类的类体,里面定义的内容称之为成员,类中的变量称之为类的属性或者类的成员变量,ClassName为类名,并且大括号后面的分号";"一定不能省略。

class ClassName

{

//类体

};

类有俩种定义方式:

  1. 声明和定义全部放在类体中(成员函数在类体中定义,编译器可能会将其作为内联函数处理)

    cpp 复制代码
    class Date 
    {
    public:
    	void addDate(int x)
    	{
    		//
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
  2. 类声明放在.h文件中,成员函数定义在.cpp文件中(成员函数前要加上类名::

    cpp 复制代码
    #pragma once
    class Date
    {
    public:
    	void addDate(int x);
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    
    #include<iostream>
    #include"date.h"
    void Date::addDate(int x)
    {
    	std::cout << "void addDate(int x);";
    }

    4. 类的访问限定符和封装

4.1 类的访问限定符

另外需要注意的是 class 的默认访问权限是 private ,而struct 的默认访问权限是 public(兼容c语言)

4.2 封装

面向对象有三大特性:封装、继承、多态 。其余俩种会在后续文章中给大家讲到。今天主要要讲的就是封装。什么是封装呢:将数据和操作数据的方法进行有机的结合,隐藏对象的属性和实现细节。仅对外公开接口来和对象进行交互。

封装本质上是对数据的一种管理,就像银行不对我们开放银行内部的处理资金的细节,仅仅对我们开放一个或者多个窗口来与我们用户进行交流,降低了我们的使用成本。封装也是如此。

5. 类的作用域

类重新向我们定义了一种全新的作用域,类的所有成员都在类的作用域中,在类体外定义成员需要使用"::"作用域操作符指明成员属于哪个类域。

cpp 复制代码
class Date 
{
public:
	void addDate();
private:
	int _year;
	int _month;
	int _day;
};
void Date::addDate()
{
	cout << "void addDate()";
}

6. 类的实例化

类的实例化就是使用类创建对象的过程,而类就相当于一张图纸,实例化就是将这个图纸实现的过程。一个类可以实例化出多个对象,实例化出来的对象占用实际的物理空间,存储类成员变量。

7. 类对象模型

7.1 类对象的存储方式

类实例化的时候只会保存成员变量,成员函数存放在公共的代码段。

我们也可以通过获取对象的大小来验证一下上面的存储方式。

cpp 复制代码
class Date 
{
public:
	void addDate()
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	cout << sizeof(d1);
}

输出:

12

代码中我们可以看出类的大小仅仅是三个int类型变量的大小,所以可以证明存储模型是正确的。并且我们的类的大小也遵从内存对齐规则。

7.2 类的大小

7.2.1 空类的大小

当我们定义一个空类的时候,实例化这个类,实例化后的对象的大小不为0,而是1,这一个字节用来占位,表示这个对象确实存在。非空类的大小就需要按照内存对齐的规则进行计算。

cpp 复制代码
class MyClass
{};
int main()
{
	MyClass mc;
	cout << sizeof(mc);
}

输出:

1

7.2.2 结构体内存对齐规则

C++中无论是结构体还是类都遵从内存对齐的原则,这里的内存对齐和c语言的结构体内存对齐完全相同,所以不在进行过多的讲解。

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

8. this关键字深入讲解

8.1 this指针的引出

在上面内容的叙述中我们得知成员函数是保存在公共代码区的,那我们实例化的对象是怎样准确的在成员函数中调用自己的成员变量的呢?这就是我们要说的this关键字,其实也就是this指针。this指针其实成员函数的第一个隐含参数,当我们实例化的对象调用的这个函数的时候,会自动将自己的地址传进去。

我们看到的代码和输出:

cpp 复制代码
class Date
{
public:
	void print()
	{
		cout << _year << "年" << _month << "月" << _day << "日";
	}
private:
	int _year = 2023;  //缺省值,用于默认构造时成员变量的默认值,会在后续构造函数中讲到
	int _month = 8;    //暂时不用太过注意
	int _day = 6;
};
int main()
{
	Date d1;
	d1.print();
}

输出:

2023年8月6日

实际上的代码(编译器处理过的):

8.2 this指针的特性

  1. this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
  2. 只能在"成员函数"的内部使用
  3. this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。
  4. this指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递
cpp 复制代码
//相当于下面的代码
class Date
{
public:
	void print(Date* const this)
	{
		cout << this->_year << "年" << this->_month << "月" << this->_day << "日";
	}
private:
	int _year = 2023;  
	int _month = 8;    
	int _day = 6;
};
int main()
{
	Date d1;
	d1.print(&d1);
}

不过还有一种特殊的情况会使我们调用print函数失败,就是我们传入的对象d1是const类型,如下:

9.空指针调用成员函数的问题

这里大家可能就要怀疑了,空指针怎么可能可以调用成员函数呢,答案是当函数里面没有发生对成员变量的解引用操作就可以调用。如下:

这里的print函数被保存在公共代码区中,并且没有发生任何的空指针访问的行为,所以可以调用成功。

cpp 复制代码
class Date
{
public:
	void print()
	{
		//这里没有在函数体内对成员函数进行操作,
        //而我们的成员函数是统一放在公共代码区的,
        //所以这里也就没有发生任何的空指针行为,所以是可以被调用成功的。
		cout << "void print()" << endl;
	}
private:
	int _year = 2023;
	int _month = 8;
	int _day = 6;
};
int main()
{
	Date* d1 = nullptr;
	d1->print();
}

输出:

void print()

而下面的print函数对空指针进行了解引用行为,所以程序就发生了崩溃。

cpp 复制代码
class Date
{
public:
	void print()
	{
		//这里对空指针进行了解引用的操作,
		//引发了空指针的非法访问,所以代码就会直接崩溃
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
		//引发了异常: 读取访问权限冲突。this 是 nullptr。
	}
private:
	int _year = 2023;
	int _month = 8;
	int _day = 6;
};
int main()
{
	Date* d1 = nullptr;
	d1->print();
}

输出:无输出

异常:引发了异常: 读取访问权限冲突。this 是 nullptr。

综上:我们就将和类有关的基础知识说的差不多了,后面我们就要开始讲解类的六大默认成员函数了,将会是一块难啃的骨头,敬请期待。码文不易,记得三连奥。

相关推荐
老猿讲编程13 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye1 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
霁月风2 小时前
设计模式——适配器模式
c++·适配器模式
萧鼎3 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸3 小时前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农3 小时前
Python 继承、多态、封装、抽象
开发语言·python