C++都有对象了,你还没有吗?

本文介绍了C++中的面向对象编程,对比了C语言的面向过程。讲解了类的概念,包括类的定义、访问限定符、封装,以及类与对象的关系。通过实例展示了类的使用,并对比了C语言中实现栈的方式,强调了C++中类的封装特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解C++中有关类和对象的初步了解.
金句分享:
✨应该有更好的方式开始新的一天,而不是千篇一律的每天醒来.✨

前言

一、面向过程与面向对象

C语言作为一种面向过程的编程语言,注重解决问题的过程和步骤,通过函数和控制流程的设计来组织程序。

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

现实实例:制作一个简单的三明治。
在这里插入图片描述

面向过程分析:(C语言)

  1. 收集所需材料和工具:面包、黄油、火腿、生菜、刀子、砧板等。
  2. 将切好的面包放在砧板上。
  3. 使用刀子涂抹黄油在面包片上。
  4. 在其中一片面包上放上火腿和生菜。
  5. 将另一片面包盖在火腿和生菜上,使之成为一个完整的三明治。
  6. 可选:将整个三明治切成两半或四等份。
  7. 完成。

面向对象分析:(C++)

  1. 定义一个"三明治"类,它具有属性(面包、黄油、火腿、生菜)和方法(涂抹黄油、放置火腿和生菜、组装成三明治)。
  2. 创建一个"三明治"对象。
  3. 调用对象的方法,按照特定的顺序执行:
  • 调用涂抹黄油的方法,在面包片上涂抹黄油。
  • 调用放置火腿和生菜的方法,在其中一片面包上放置火腿和生菜。
  • 调用组装成三明治的方法,将另一片面包盖在火腿和生菜上。
  1. 可选:调用切割的方法,将整个三明治切成两半或四等份。
  2. 完成。

面向过程分析中,我们按照步骤逐一执行操作,强调流程和步骤的线性顺序。

而在面向对象分析中,我们将问题抽象为一个对象,该对象具有属性和方法,通过调用对象的方法来实现功能,强调对象的行为和内部状态的封装。

总之面向对象以后,重点不再关注做事的具体过程,而是关注其中涉及哪些对象

二、类

2.1 类的介绍

还记得C语言阶段学习过的结构体吧?在结构体中我们可以定义各种类型的变量,但是我们不能在结构体中定义函数.

C语言中:
在这里插入图片描述
同样一段代码在C++中,结构体内不仅可以定义变量,也可以定义函数。

C++中:
在这里插入图片描述
为什么呢?因为C++中将结构体升级为了==“类”.在类==中是可以定义函数的,通常被称为成员函数.

C++中,class关键字用于定义一个类。类是一个用户定义的数据类型。
类体中内容称为类的成员:可以包含属性(成员变量)和操作/方法(成员函数)。

2.2 类的定义方式

使用class关键字可以创建一个新的类,并定义它的特征(如数据成员和成员函数)。类可以用于封装数据和行为,并提供对外部程序的接口。通过类的实例化,可以创建对象,并访问其成员变量和成员函数。在面向对象编程中,类是非常重要的一个概念,它使得程序更加模块化,易于维护和扩展。

(1)声明和定义全部放在类体中.

注意:成员函数如果在类中定义,编译器默认是按内联函数(inline)处理.(同样如果函数体过长也是不会产生内联的.)


#include <iostream>

using namespace std;//在工程代码中不建议展开可能会产生命名冲突

class Person {
public:
    // 构造函数(后面会讲,这里按普通成员函数理解)
    Person(char* n, int a) {
        name = n;
        age = a;
    }

    // 成员函数
    void introduce() {
        cout << "欢迎来到CSDN!\n我是" << name << ",我的年龄是" << age << "岁" << endl;
    }
private:
    //成员变量
    char* name;
    int age;
};

int main() {
    char name[] = "初阶牛";
    Person person(name, 18);
    person.introduce();

    return 0;
}

运行结果:

欢迎来到CSDN!
我是初阶牛,我的年龄是18

这个类的名字叫做 Person,它有两个私有成员变量:nameage

类还有一个公有的成员函数:introduceintroduce 函数用于打印出个人信息,即打印出对象的 nameage 属性。

main 函数中,我们创建了一个名为 personPerson 对象,并通过构造函数初始化了它的成员变量。然后我们调用了 introduce 函数来展示个人信息。

通过使用成员函数和成员变量,我们可以对对象进行操作和访问其属性,从而使类具有更多的功能和灵活性。请注意,在 C++ 中需要使用 iostream 库进行输入输出操作,并使用 main 函数创建类的对象并调用成员函数。

(2)类的声明和"成员函数"分离

即类声明放在.h文件中,成员函数定义放在.cpp文件中.

注意:成员函数名前需要加类名::
在这里插入图片描述

2.3 类的访问限定符

C++中,类的访问限定符(访问修饰符)用于控制类的成员对外部代码的可见性和访问权限。C++提供了三个主要的访问限定符:publicprivateprotected

  1. 公共访问(public):使用public关键字来指定。公共成员可以从任何地方访问,包括外部代码和其他类。公共成员在整个程序中可见。

  2. 私有访问(private):使用private关键字来指定。私有成员只能在声明它们的类内部访问。其他任何外部代码或其他类都无法直接访问私有成员,包括子类。

  3. 受保护访问(protected):使用protected关键字来指定。受保护成员只能在声明它们的类内部访问以及该类的子类中访问。外部代码无法直接访问受保护成员。

在这里插入图片描述

我们暂时这里将私有访问(private )和受保护访问(protected)看作相同的,后续再区分.

注意:

  1. C++中class(类)的默认访问级别是私有访问(private)。类的成员将默认为私有成员,只能在类内部访问。
  2. struct(结构体)为public(因为struct要兼容C语言),在C语言中,外部可以访问结构体中的成员变量.

访问限定符的选择取决于设计需求和封装原则。公共成员允许类的用户直接访问,而私有成员则隐藏了实现细节并提供了更好的封装。受保护成员专门用于派生类访问,并且在类外部不可见。

2.4 封装的介绍

封装的定义:(灰常重要)

是指将数据方法放在一起.将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

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

在这里插入图片描述

在现实世界中,手机是一个复杂的设备,它包含了许多内部组件和功能,如屏幕、摄像头、声音、通信等。对于我们普通用户来讲,手机只需要提供给我们我们点击的屏幕,和手动控制开关机的按键就可以了,它内部具体是怎么实现功能的我们并不关心,如果让用户去关心CPU如何设计,主板上的线路如何布局,这显然是不合理的,手机的封装也就体现了管理,帮助用户更方便的使用手机

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

三、“类” 与 “对象” 之间的关系

我们先看一下test2函数.

class Person {
public:
    //成员函数
    Person(char* n, int a);
    void introduce();

private:
    //私有成员变量
    char* name;
    int age;
public:
    float weight;
};

void test2()
{
    //报错
    Person.weight = 60.5;//报错,类只是声明,并没有申请空间,不能用于存放数据
    
    //正确写法
    char name[] = "初阶牛";
    Person cjn(name, 18);//通过类实例化出 cjn这个对象.
    cjn.introduce();
    cjn.weight = 60.5;//实例化出来的对象是有空间的,可以存储数据

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

  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

有一个很形象的比喻,就好比是建筑的图纸,只是一些声明,并不没有去申请实际的空间,就好比图纸只是设计形状,并没有占有空间.不能存储数据,就类似于图纸不能住人.
通过类实例化出的对象后就分配的实际空间,对象可以用于存储数据,就像图纸设计出来房子后,房子里面就可以住人了.
在这里插入图片描述

3.1 类的大小计算

试着猜一下下面People类的大小.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

class People
{
public:
	void Print()
	{
		int a = 0;
		double b= 1.2;
		cout << _name << endl;
	}
private:
	char _name;
	int _age;
};

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

在这里插入图片描述
运行结果:

8

解释:
为什么是8呢?因为类在计算大小时也要考虑内存对齐.
char _name占1个字节(偏移量为0),int _age占四个字节(4-7偏移量).共八个字节.
为什么不计算成员函数的大小呢?

那就要说到类的设计方式了,因为成员函数消耗的内存相对都比较大,而每个对象都是使用同一个成员函数,如果每个对象都给成员函数开辟空间,这就比较浪费了,所以C++中的类采用下图这种方式存储:
在这里插入图片描述
将;类的成员函数放在公共代码段,需要使用的时候调用即可,对象之间公用同一个成员函数.这种设计方式有效的节省了类实例化出对象后的空间消耗.

那小伙伴掌握如何计算类的大小了吗?

不妨猜一下下面A类和B类的大小.

// 只有成员函数的类
class A {
public:
	void test() {}
};
//空类
class B

{};

int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	return 0;
}

在这里插入图片描述
运行结果:

1
1

这是因为没有成员变量的类或者空类也是会在占用一个字节,因为需要占位,表示对象的存在.

3.2 this指针

#include <iostream>

using std::cout;
using std::cin;
using std::endl;


class Person {
public:
    //成员函数
    Person(char* n, int a)
    {
	    _name = n;
	    _age = a;
    }

    void introduce()
    {
        cout << _name << endl;
    }

private:
    //私有成员变量
    char* _name;
    int _age;
};

int main()
{
    char name1[] = "初阶牛";
    char name2[] = "CSDN";
    Person person1(name1, 18);
    Person person2(name2, 18);
    //这两个调用的是同一个函数吗?
    person1.introduce();
    person2.introduce();
	return 0;
}

运行结果:

初阶牛
CSDN

上面这段代码中这两个调用的是同一个函数吗?如果是同一个,为什么打印的结果却不一样?

    person1.introduce();
    person2.introduce();

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

没理解的看过来:
在这里插入图片描述

其实就是编译器帮我们传了参,不需要用户进行手动传参.

结构体内存对齐复习:传送门

3.3 深入理解this指针

为了深入理解this指针,下面有两道题可以做一下:

第一题:this指针本身存储在哪里?

A. 栈区
B. 堆区
C. 对象中
D. 常量区

在这里插入图片描述
答案:

栈区,因为this指针就是一个形参,只不过是被编译器默认传递,形参是存放在栈区的 函数栈帧建立时压栈,函数结束时,销毁.

第二题:下面这两段代码分别会出现什么情况?

//代码1:
class A
{
public:
	void Print()
	{
		cout << "HELLO CSDN" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}
// 代码2:
class A
{
public:
	void Print()
	{
		cout << _age << endl;
	}
private:
	int _age;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

在这里插入图片描述

答案:
代码1:
正常运行,虽然this是空指针,但是并没有对this指针进行解引用,传递空指针是不会报错的.
代码2:
运行崩溃,对this空指针进行解引用,属于非法访问.
运行图如下:在这里插入图片描述

四、C与C++对比

对比C语言,帮助更好的理解C++的封装特性.

C语言数据和方法是分离的,给予C程序员很大的操作空间.这样也就使得对C程序员的要求很高.太自由了!
比如:
对于一个用C语言实现的栈.很多数据在栈的外部可以被随意的修改和使用,这样就对程序员的要求极高.对于不规范的编程,(一会通过接口(函数),一会自己在外界直接访问)很容易造成混乱

C++程序员受封装的保护,对于栈中的很多操作只能通过调用对应的接口实现,更好的约束了程序员的操作规范

C实现栈:

//C语言版本
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int DataType;
typedef struct Stack
{
	DataType* array;
	int capacity;
	int size;
}Stack;
void StackInit(Stack* ps)
{
	assert(ps);
	ps->array = (DataType*)malloc(sizeof(DataType) * 3);
	if (NULL == ps->array)
	{
		assert(0);
		return;
	}
	ps->capacity = 3;
	ps->size = 0;
}
void StackDestroy(Stack* ps)
{
	assert(ps);
	if (ps->array)
	{
		free(ps->array);
		ps->array = NULL;
		ps->capacity = 0;
		ps->size = 0;
	}
}
void CheckCapacity(Stack* ps)
{
	if (ps->size == ps->capacity)
	{
		int newcapacity = ps->capacity * 2;
		DataType* temp = (DataType*)realloc(ps->array,
			newcapacity * sizeof(DataType));
		if (temp == NULL)
		{
			perror("realloc申请空间失败!!!");
			return;
		}
		ps->array = temp;
		ps->capacity = newcapacity;
	}
}
void StackPush(Stack* ps, DataType data)
{
	assert(ps);
	CheckCapacity(ps);
	ps->array[ps->size] = data;
	ps->size++;
}
int StackEmpty(Stack* ps)
{
	assert(ps);
	return 0 == ps->size;
}
void StackPop(Stack* ps)
{
	if (StackEmpty(ps))
		return;
	ps->size--;
}
DataType StackTop(Stack* ps)
{
	assert(!StackEmpty(ps));
	return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->size;
}
int main()
{
	Stack s;
	StackInit(&s);
	StackPush(&s, 1);
	StackPush(&s, 2);
	StackPush(&s, 3);
	StackPush(&s, 4);
	printf("%d\n", StackTop(&s));
	printf("%d\n", StackSize(&s));
	StackPop(&s);
	StackPop(&s);
	printf("%d\n", StackTop(&s));
	printf("%d\n", StackSize(&s));
	StackDestroy(&s);
	return 0;
}

C++实现栈:

typedef int DataType;
class Stack
{
public:
	void Init()
	{
		_array = (DataType*)malloc(sizeof(DataType) * 3);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = 3;
		_size = 0;
	}
	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}
	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }
	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity *
				sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);

	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Pop();
	s.Pop();
	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Destroy();
	return 0;
}

在这里插入图片描述

最后补充一个小知识:
局部域和全局域会影响生命周期
类域和命名空间域不会影响生命周期.

评论 111
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

初阶牛

感谢佬的支持,有你真好!

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

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

打赏作者

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

抵扣说明:

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

余额充值