首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >类和对象(上):类的定义、实例化、this指针、对比C++/C两种语言实现Stack

类和对象(上):类的定义、实例化、this指针、对比C++/C两种语言实现Stack

作者头像
用户11831438
发布2025-12-30 13:32:13
发布2025-12-30 13:32:13
1400
举报
一、类的定义
1.1 类定义格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省

略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或

者成员函数。

上图中所展示的内容仅仅是一个类的定义格式,定义的也只是一个类型,如果想使用这个类,我们可以使用这个类定义对象,在C++中类的名字就是类型,直接使用类名创建对象:

不知道是否有细心的小伙伴发现上图中紫色方框内的两行代码好像有那么一点问题,好像我们现在还不能访问成员变量和成员函数,这是为什么?

我们知道,C++是面向对象的,面向对象的三大特性:封装、继承、多态。C++引入封装的概念,在C语言中数据和方法是分开的,而在C++中数据和方法是放在一起的,并且都在类里面。

封装的特点:

  1. C++中数据和方法放到一起,都在类里面
  2. 访问限定符

接下来,我们来看看访问限定符的相关内容:

1.2 访问限定符

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

那访问限定符有哪些呢?

public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,在类中可以直接被访问,protected和private是一样的,在以后继承章节才能体现出啊他们的区别。

ok,那我们现在是不是可以解决前面的问题了:

既然我们已经学习了访问限定符相关的知识,那为什么当我们在主函数调用成员对象时,还会出现错误呢? 原因:public修饰的成员在类外可以直接被访问,也就是public修饰的成员是公共的,类里类外都是可以使用的;而protected和private修饰的成员在类外不能直接被访问,类里可以直接使用,protected和private是一样的 无访问限定符时,class里面的成员默认是私有的(public),类外不能被使用,类里面可以使用

注意:一般情况下,成员变量为私有,成员函数为公有

C++->数据和方法是在一起的,都放在类中,封装的本质体现了更规范的管理

1.3 struct定义类

有同学看到这个标题,会感到有那么一点点的小疑惑?我们知道在C语言中,struct是定义结构体的,当我们使用结构体创建对象的时候都要加上struct,这使程序员感到很麻烦,于是在C++中既兼容C中struct的用法,也给struct升了级--C++中struct可以定义类。

C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类。 定义在类里面的成员函数默认为inline。 补充:定义在类里面默认为inline(默认就是内联函数,至于最终会不会变成内联函数,这个还是得看编译器我们上篇文章已经做过解释了,VS是10行左右就不会变成内联函数了)。

1.4 命名规范

为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加_ 或者 m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求

谷歌、阿里巴巴的_是加在后面的,百度的_则是加在前面的。

m_ / m + 首字母大写(如mCapacity,再比如m_capacity)。

命名规范这一块C++没有强制要求!!!

为了区分形参和成员变量,我们要给成员变量一个明显的标识——在成员变量的前面加 “_”

比如前加_或者后加_,就会更好区分。

两大命名规范: (1)驼峰命名法(Camel Case)

驼峰命名法是编程中一种常见的变量命名方式。

特点:单词之间不使用空格或下划线,而是通过每个单词首字母大写来分隔单词,看起来像驼峰的脊背,故此得名“驼峰命名法”。

其实驼峰命名法还分小驼峰命名法(lowerCamelCase)和大驼峰命名法(UpperCamelCase / PascalCase),我们简单了解一下——

  1. 小驼峰命名法是第一个单词首字母小写,后面每个单词的首字母大写(常用于变量名、函数名);
  2. 大驼峰命名法是每个单词首字母都大写,包括第一个单词(用于类名、构造函数名等)。

(2)蛇形命名法(snake_case) 蛇形命名法是指单词间用下划线连接的风格,全部小写——

1.5 类域

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

并且类是支持声明和定义分离的,这样做的目的是方便维护代码

.h文件里面放成员变量和成员函数的声明

.c文件里面放成员函数的定义:

在这里我想提一个问题,为什么我们需要在上图函数定义的前面加上“Stack ::” ?

类域影响的是编译的查找规则,上图程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,如果在局部域和全局域中找不到array等成员,就会到类域中去查找。

类域的查找是在整个类中查找!!!

补充:

  • 局部和全局会影响生命周期 原因:因为他会影响对象存在不同的区域里面
  • 命名空间、类域不会影响生命周期,只会影响同一个域内不能创建同名变量,不同域中可以创建同名变量

举个例子: 下面的栈和队列是不是都有叫Init、push、pop的函数,那么构不构成重载?不构成。 为什么? 前面说了,不同的域可以创建同名变量。这些同名的函数包括参数可以同时存在,因为它们是存在不同的域里面,两个域里面可能会定义同名的函数或者同名的变量,因此不存在两个类的这些变量、函数冲突的,因为它们有一个独立的作用域。

在上图中的成员变量那块,成员变量是声明还是定义?

ok,那怎么看是声明还是定义呢?那就看是否开辟空间,如果没有开辟空间就是声明,如果开辟了空间就是定义。所以这么来看,上图中的成员变量那块仅仅是声明,不是定义

通过上面的提问,就引出了实例化的知识:

二、实例化
2.1 实例化的概念

用类型在物理内存中创建对象的过程,称为类实例化出对象。

类是对象进行一种抽象描述,是一个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。

这时候就会有同学会问到,难道这个Stack类型只能创建出一个实例化对象吗?当然不是

一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间大小、功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房⼦,房子才能住人。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存才能存储数据。

代码演示:

代码语言:javascript
复制
class Stack
{
public:
	//成员函数
	void Init(int n = 4)
	{ }
	void Push(int x)
	{ }
private:
	// 成员变量,仅仅是声明
	int* array;
	size_t capacity;
	size_t top;
};
int main()
{
	Stack s1;//定义
	Stack s2;
	Stack s3;
	return 0;
}
2.2 对象的大小

那我们该如何计算对象的大小呢?对象的大小遵从内存对齐的规则

内存对齐规则(重要)

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处(第一个成员总是放在偏移量为0的地址上)
  2. 从第二个成员变量开始,都要对齐到某个对齐数的整数倍的地址处 对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。(vs中默认的值为8,Linux中gcc没有默认对齐数,对齐数就是成员自身的大小)
  3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中的最大的)的整数倍
  4. 如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

详细步骤请看:自定义类型:结构体、联合体、枚举-CSDN博客

测试代码:

代码语言:javascript
复制
#include<iostream>
using namespace std;
class Stack
{
public:
	//成员函数
	void Init(int n = 4)
	{ }
	void Push(int x)
	{ }
private:
	// 成员变量
	int* array;
	size_t capacity;
	size_t top;
};
int main()
{
	Stack s1;
	cout << sizeof(s1) << endl;
	cout << sizeof(Stack) << endl;
	return 0;
}

通过运行结果和动手计算,发现结果都是24,并且这个24仅仅是成员变量的大小。

这时候,就会有同学会有疑问?计算对象大小的大小和计算成员变量的大小是一样的,那这个成员函数是不是就没存在对象里面?

ok,对象的大小只考虑成员变量,不考虑成员函数,我们是不需要将这个成员函数的指针存在对象里面的

为啥我们不需要将这个成员函数的指针存在对象里面?

这是因为方框内的函数调用s1.Init()和s2.Init(),调用的是同一个函数,若每个对象中都要存一份函数的地址,是对空间的浪费,函数的地址在编译或链接时,就已经确定了函数的地址,若存在对象里是一种浪费

下面是计算对象大小的代码演示:

代码语言:javascript
复制
#include<iostream>
using namespace std;
 
class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};
 
class B
{
public:
	void Print()
	{
		//...
	}
};
 
class C
{ };
 
int main()
{
	cout << sizeof(A) << endl;
	//开1个byte的空间是为了占位,占位不存储实际数据,表达对象存在过(占位标识对象存在)
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
 
	B b1;
	B b2;
	cout << &b1 << endl;
	cout << &b2 << endl;
 
	return 0;
}

我们看到输出结果中有两个1,这么什么情况?

上面的程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个字节呢?这是因为开1byte是为了占位,不存储实际数据,表示对象存在过

接下来我们来看一段代码:

代码语言:javascript
复制
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;
	Date d2;
	d1.Init(2025, 8, 2);
	d1.Print();

	d2.Init(2025, 7, 2);
	d2.Print();
	return 0;
}

我们看到上面代码是可以正常运行的,但是不知道有没有同学会感到疑惑?我们知道函数地址是不会存在对象里面,而是在编译或者链接的时候就已经确定了,d1.Init(),d1.Print()和d2.Init(),d2.Print()调用的都是同一个函数,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢? 其实编译器是知道的,这里会使用到一个隐含的this指针

三、this指针

编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加一个当前类类型的指针,叫做this指针。

类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this>_year = year; C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针

其实上图中的this指针的写法还是有一点瑕疵的,真实的this指针应该是这么写的:

学完了this指针,那让我们来看几道题:

解析:

注意:空指针、野指针是运行错误,运行逻辑错误,编译器只会给个warning!!!

总结:空指针不犯错,解引用空指针才犯错

四、对比C++/C两种语言实现Stack

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

三大特性之中,我们现在只是了解了一下封装,剩下两个后面再介绍。

⾯向对象三⼤特性:封装、继承、多态,下⾯的对⽐我们可以初步了解⼀下封装。

通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化。

  • C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这里的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习
  • C++中有⼀些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef,用类名就很方便
C实现Stack代码:
代码语言:javascript
复制
#define  _CRT_SECURE_NO_WARNINGS  1
 
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;
void STInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}
void STDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
	assert(ps);
	// 满了, 扩容
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity *
			sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}
 
bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
 
void STPop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	ps->top--;
}
 
STDataType STTop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	return ps->a[ps->top - 1];
}
 
int STSize(ST* ps)
{
	assert(ps);
	return ps->top;
}
 
int main()
{
	ST s;
	STInit(&s);
	STPush(&s, 1);
	STPush(&s, 2);
	STPush(&s, 3);
	STPush(&s, 4);
	while (!STEmpty(&s))
	{
		printf("%d\n", STTop(&s));
		STPop(&s);
	}
	STDestroy(&s);
	return 0;
}
C++实现Stack代码:
代码语言:javascript
复制
#define  _CRT_SECURE_NO_WARNINGS  1
 
#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	// 成员函数 
	void Init(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
 
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	void Pop()
	{
		assert(_top > 0);
		--_top;
	}
	bool Empty()
	{
		return _top == 0;
	}
	int Top()
	{
		assert(_top > 0);
		return _a[_top - 1];
	}
	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	// 成员变量 
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);
	while (!s.Empty())
	{
		printf("%d\n", s.Top());
		s.Pop();
	}
	s.Destroy();
	return 0;
}

观察上面两段代码,在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力。

总结:本文详细介绍了C++中类的定义与使用。主要内容包括:1)类的定义格式和成员变量/函数的访问控制(public/protected/private);2)struct在C++中也可以定义类;3)成员变量命名规范建议;4)类的作用域和::操作符的使用;5)类的实例化过程与对象内存分配;6)this指针的工作原理;7)通过对比C和C++实现栈的代码,展示C++面向对象封装特性的优势。文章从基础语法到实际应用,系统讲解了C++类的核心概念和使用方法。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、类的定义
    • 1.1 类定义格式
    • 1.2 访问限定符
    • 1.3 struct定义类
    • 1.4 命名规范
    • 1.5 类域
  • 二、实例化
    • 2.1 实例化的概念
    • 2.2 对象的大小
  • 三、this指针
  • 四、对比C++/C两种语言实现Stack
    • C实现Stack代码:
    • C++实现Stack代码:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档