【C++】类和对象详解(类的使用,this指针)

文章目录


前言

提示:这里可以添加本文要记录的大概内容:

在计算机编程领域,程序设计的方法论不断演化,从最初的面向过程到如今更为强大而灵活的面向对象。本文将深入探讨C++中关于类和对象的概念,为读者提供对面向对象编程的深刻理解。

提示:以下是本篇文章正文内容,下面案例可供参考

面向过程和面向对象的初步认识

在计算机编程的世界中,面向过程和面向对象是两种不同的编程范式,它们反映了程序设计的不同思想和方式。以下是对这两种范式的初步认识:

面向过程:

面向过程编程是一种以过程为中心的编程思想。在这种范式中,程序被看作一系列的函数或过程的集合,这些函数按照一定的次序依次执行,完成特定的任务。程序的执行流程主要由函数的调用关系来控制。

主要特点包括:

  • 程序的控制流程是线性的,从上到下逐行执行。
  • 重点在于解决问题的步骤和流程,以完成特定的任务。
  • 数据和函数分离,强调过程的执行。

面向对象:

面向对象编程是一种以对象为中心的编程思想。在这种范式中,程序被组织成一组对象,每个对象包含数据和相关的操作,通过这些对象之间的交互来完成任务。

主要特点包括:

  • 程序的控制流程不再是线性的,而是由对象之间的消息传递和方法调用构成。
  • 重点在于组织和管理对象,强调数据的封装和对象的行为。
  • 对象可以被看作是现实世界中的实体,有着状态和行为。

对比和选择:

面向过程和面向对象各有优缺点,选择合适的范式取决于问题的性质和解决方案的需求。面向过程适用于简单的、线性的问题,而面向对象更适用于复杂的、模块化的系统设计。

在实际开发中,很多编程语言提供了同时支持面向过程和面向对象的特性,如C++、Java等,使得程序员可以根据具体情况选择合适的编程方式。这也说明了在面向对象编程的潮流中,面向过程并没有被淘汰,而是与面向对象共同存在,相互补充,为编程提供了更灵活的选择。

类的引入

在c语言中,结构体内只能定义变量,在c++中,结构体内不仅可以定义变量,还可以定义函数

cpp 复制代码
#include <iostream>
#include <cstring>  // 添加头文件以使用字符串库函数
using namespace std;

struct stu
{
    void getName()
    {
        cout << name << endl;
    }
    int age;
    int num;
    char name[20];
};

int main()
{
    struct stu s1;
    strcpy(s1.name, "dzj");  // 使用strcpy将字符串赋值给字符数组
    s1.getName();
    return 0;
}

但是在C++中,上述结构体的定义,更喜欢用class来代替

注意:C语言结构体中不能定义成员函数,C++兼容C的语法,并进行拓展,可以定义成员函数

类的定义

类名:由成员函数和成员变量组成,同时注意分号

cpp 复制代码
class className
{
	//类名:由成员函数和成员变量组成
}

class是定义类的关键字,className是类的名称,{}中类的主体同时注意类定义结束时后面的分号。类中的元素是类的成员:类中的数据称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数
类的两种定义方式:

  • 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
cpp 复制代码
class person
{
public:
    void print()
    {
        cout << _age << _name << _weight << endl;
    }
private:
    int _age;
    char _name[10];
    int _weight;
};
  • 声明放在.h中,类的定义放在.cpp中


一般情况下,一般采用第二种定义和声明分离的方式,但是为了方便,在平时的练习时,可以采用第一种方式(声明和定义全部放在类体中)

补充:声明和定义的区别是什么?
声明是一种承诺,承诺要干什么,但是还没做,定义就是把这个事情执行了或者落实了

类的访问限定符和封装性

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

访问限定符

  1. public(公有)
  2. protected(保护)
  3. private(私有)

【访问限定符说明】

  • public修饰的成员在类外可以直接被访问
  • protected修饰的成员类的内部和派生类的成员可以访问,但在类的外部无法直接访问.
  • private修饰的成员只能在本类中访问,在类外不能直接访问
  • 访问权限的作用域从该访问限定符出现的位置直到下一个访问限定符出现为止
  • class的默认访问权限是private,但是struct是public(因为struct要兼容c语言)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

C++中struct和class的区别是什么?

C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。

和class是定义类是一样的,区别是struct的成员默认访问方式是public,class的成员默认访问方式是private。

封装性

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

在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?

封装是面向对象编程(OOP)中的一个重要概念,它指的是将数据(变量)和操作数据的方法(函数)捆绑在一起的一种机制。封装有助于隐藏对象的内部实现细节,同时提供一组公共接口供外部使用。这样,对象的内部状态和行为对外部来说是不可见的,只有通过公共接口才能与对象进行交互。

封装的主要目的有以下几点:

  1. 数据隐藏: 将对象的实现细节隐藏起来,防止外部直接访问对象的内部数据,从而提高数据的安全性。

  2. 实现细节隔离: 允许对象实现者修改对象的内部实现细节而不影响外部使用者的代码。外部代码只需要关心对象的接口,而不必关心其具体实现。

  3. 代码组织: 将数据和操作数据的方法封装在一个单元中,使得代码更加模块化、可维护性更强。

在面向对象的语言中,封装通过类来实现。类封装了数据成员和成员函数,成员函数提供了访问和操作数据的接口。访问控制修饰符(如public、private、protected)用于控制成员的可见性和访问权限,实现封装的关键。

示例:

cpp 复制代码
#include <iostream>

class Car {
private:
    // 私有数据成员
    int speed;

public:
    // 公有成员函数
    void setSpeed(int s) {
        if (s >= 0) {
            speed = s;
        }
    }

    int getSpeed() const {
        return speed;
    }
};

int main() {
    Car myCar;

    // 使用公有接口设置速度
    myCar.setSpeed(60);

    // 使用公有接口获取速度
    std::cout << "Current Speed: " << myCar.getSpeed() << " km/h" << std::endl;

    return 0;
}

在这个例子中,Car 类封装了一个私有的速度成员,并提供了公有的 setSpeedgetSpeed 方法来设置和获取速度。外部代码通过这些公有接口与 Car 对象进行交互,而不需要了解具体的实现细节。这就体现了封装的思想。

类的作用域

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

cpp 复制代码
class person
{
public:
    void print();
private:
    int _age;
    char _name[10];
    int _weight;
};
void person::print()
{
    cout << _age << _name<<_weight << endl;
}

类的实例化

类的实例化指的是创建类的对象。在面向对象编程中,类定义了一种数据类型,而实例化则是根据这个数据类型创建具体的对象。对象是类的一个实例,具有类定义的属性和行为。

下面是一个简单的C++示例,演示了如何定义一个类并实例化对象:

cpp 复制代码
#include <iostream>
#include <string>

// 定义一个简单的Person类
class Person {
public:
    // 公有成员函数,用于设置和获取姓名
    void setName(const std::string& n) {
        name = n;
    }

    std::string getName() const {
        return name;
    }

private:
    // 私有数据成员,姓名
    std::string name;
};

int main() {
    // 实例化Person类的对象
    Person person1;

    // 使用公有接口设置姓名
    person1.setName("Alice");

    // 使用公有接口获取姓名
    std::cout << "Person's Name: " << person1.getName() << std::endl;

    // 实例化另一个Person类的对象
    Person person2;

    // 使用公有接口设置姓名
    person2.setName("Bob");

    // 使用公有接口获取姓名
    std::cout << "Person's Name: " << person2.getName() << std::endl;

    return 0;
}

在这个例子中,Person 类定义了一个私有数据成员 name 和公有的成员函数 setNamegetName。通过实例化两个 Person 对象 person1person2,分别设置和获取了它们的姓名。

实例化是面向对象编程中的重要概念,它允许我们根据类的定义创建多个对象,每个对象都拥有自己的状态和行为。

类对象模型

如何计算类对象的大小

cpp 复制代码
class A
{
public:
 	void PrintA()
 {
	 cout<<_a<<endl;
 }
private:
	 char _a;
};

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
答:对象中只存储成员变量,不存储成员函数

类对象的存储方式猜测

  • 对象中包含类中的各个成员(成员函数和成员变量)
    事实上,这样是不合理的,每个对象中成员变量是不同的,但是调用的却是同一个函数,如果按照这种方式存储,当一个类创建多个对象的时候,每个对象中都会保存一份代码,相同的代码保存了多次,浪费空间。
  • 只保存成员变量,成员函数保存在公共的代码段
    图解:

    显然计算机采用的是第二种存储方式(类中对象仅仅存储成员变量)

结论:一个类的大小,实际就是该类中"成员变量"之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

结构体内存对齐规则

关于内存对齐可以看这篇博客

this指针

this指针的引出

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	void setDate(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void getInfo()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;
	d1.setDate(2024, 1, 6);
	d1.getInfo();
	return 0;
}

问题:在上述代码中,有两个成员函数setDate和getInfo,函数体中没有关于类的划分,那么程序是怎么知道d1.setDate(2024, 1, 6)设置的就是d1的空间,而不是d2的空间
C++中通过引入this指针解决该问题,即:C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

图解

这里编译器偷偷帮我们做了处理,但是我们自己并不知道,即在成员函数第一个参数添加 类名*this,并在函数调用的时候传递了对象的地址。

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	void setDate(int year,int month,int day)//隐含的this指针Date* this
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void getInfo()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;
	d1.setDate(2024, 1, 6);//隐含的传递对象地址 d1.setDate(&d1,2024, 1, 6)
	d1.getInfo();
	return 0;
}

this指针的特性

  1. this指针的类型:类名*const
  2. this指针只能在"成员函数"的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数的时候,将对象地址作为实参传递给this形参,所以对象中不存储this指针
  4. this指针是成员函数第一个隐含的指针形参,(vs2022下)由编译器通过ecx寄存器自动传递,不需要用户传递

this指针存储在哪里?

上面说到this指针本质是成员函数的第一个形参,那么肯定存放在进程地址空间中的栈区,但是在vs2022下是存放在ecx寄存器中,不同编译器和环境下可能会有差异

this指针可以为空吗?

通过一段代码来说明

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}

	void Show()
	{
		cout << "Show()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	//p->PrintA();
	p->Show();
}

上面代码中,p->PrintA()会发生程序崩溃,但是p->Show()可以正常运行。因为在PrintA()中发生了隐含的this指针的解引用操作(对空指针的解引用),但是在Show()并没有解引用所以程序没问题

总结

通过本文的学习,我们深入了解了C++中类和对象的方方面面,从面向过程的初步认识到类的引入和定义,再到类的作用域、实例化、访问限定符及封装,以及类对象大小的计算和类成员函数的this指针。这些概念和技术不仅为我们提供了更灵活的程序设计手段,也为构建更为可维护和可扩展的软件系统奠定了基础。在今后的编程学习和实践中,希望读者能够充分运用这些知识,编写出更高效、健壮的C++程序。面向对象编程不仅仅是一种技术,更是一种思想,期待读者通过不断实践,能够更深刻地理解和运用面向对象的优秀编程范式。

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试