首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++——类和对象(1)

C++——类和对象(1)

原创
作者头像
李昂
发布2025-10-11 16:57:15
发布2025-10-11 16:57:15
1200
举报
文章被收录于专栏:C++C++

一. 类的定义

类(Class)是一种自定义的数据类型。它像一个 “蓝图” 或 “模板”,用来描述具有相同属性(数据)和行为(函数)的一组对象的共同特征。

你可以把它想象成建造房子的设计图纸

  • 图纸 (class):描述了房子有多少房间、墙是什么材料、有几层楼(这是属性),以及房子可以用来居住、出租等(这是行为)。图纸本身不是房子。
  • 房子 (Object/Instance):根据这份图纸建造出来的具体建筑。你可以根据同一份图纸建造多栋一模一样的房子。这些具体的房子就是对象实例

在 C++ 中,对象是类的具体化,类是对象的抽象

在 C 语言等面向过程的语言中,数据和操作数据的函数是分离的。当项目变得复杂时,这种方式会导致代码结构混乱,难以维护。C++语言是面向对象的,面向对象有三大特征:封装、继承、多态,C++中的类就起到了封装的作用,数据和方法放到了一起,都放在了类里面,能够更好的管理和使用

1.1 类定义的格式

  • class为定义类的关键字,后面跟类名,{}中为类的主题,注意类定义结束时后面的分号不能省略。类中内容称为类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或成员函数。
  • 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者m 开头,注意C++中这个并不是强制的,只是一些惯例
  • C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
  • 定义在类里面的成员函数默认为inline(内联函数)
代码语言:javascript
复制
// 定义一个类
class Stack
{
public:
    // 成员函数
    void Init(int n)
	{
		_arr = (int*)malloc(sizeof(int) * n);
		_capacity = n;
		_top = n;
	}

	void Push(int x)
	{
		// ...
	}
	// ...
private:
    // 成员变量
    // 为了区分成员变量,⼀般习惯上成员变量,会加⼀个特殊标识,如_ 或者 m开头 
    // int* m_arr;
	int* _arr;
	int _top;
	int _capacity;
}; // 分号不能省略

//代码规范
//驼峰法 StackInit  类型,函数    首字母大写开头+单词大写区分
//       initCapacity  变量      单词小写开头+单词首字母大写

//       stack_init
//		 init_capacity

C++将struct升级为了类,在C++中struct中也可以定义函数,类名就表示类型;同时C++中也兼容C中struct的用法,因为C++就是在C的基础上优化的,所以它一般都可以兼容C中的许多东西

代码语言:javascript
复制
// 兼容C中结构体的用法
typedef struct A
{
	// 没访问限定符,默认是public
	int _a1;
}AA;

// 升级成了类,内部可以定义函数
struct B
{
	int _b1;
	void Init()
	{}
};

//一般都用类,也有用struct的情况,比如链表
// 不再需要typedef,ListNode就可以代表类型 
struct ListNode
{
	int _val;
	// struct ListNode* next;
	ListNode* next; // 类名就是类型,不用加struct
};

1.2 访问限定符

  • C++⼀种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限 选择性的将其接提供给外部的用户使用。
  • public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问
  • 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后⾯没有访问限定符,作用域就到 } 即类结束。
  • class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
  • 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
代码语言:javascript
复制
class A
{
	// 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止
    // 如果后⾯没有访问限定符,作用域就到 } 即类结束
    // 一般情况就是在同一个访问限定符的放在一起
public:
	void func1()
	{}

private:
	int _a1;

public:
	int _a2;
};

1.3 类域

在C++中有局部域、全局域、类域、命名空间域。其中,局部域和全局域会影响生命周期,因为变量在函数栈帧销毁后也会销毁,相同的域不能定义同名变量,同名函数(函数重载除外),不同的域可以。

  • 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
  • 类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知 道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。

类中成员函数声明和定义分离时,定义函数时要指定类域

代码语言:javascript
复制
// Stack.h
#include<iostream>
using namespace std;

class Stack
{
public:
	void Init(int n = 4);
private:
	int* _array;
	int _top;
	int _capacity;
};

// Stack.cpp
// 类中成员函数声明和定义分离时,定义函数时要指定类域
void Stack::Init(int n)
{
 array = (int*)malloc(sizeof(int) * n);
 if (nullptr == array)
 {
 perror("malloc申请空间失败");
 return;
 }
 capacity = n;
 top = 0;
}

二. 实例化

2.1 实例化的概念

  • 用类类型,在物理内存中创建对象的过程,称为类实例化出对象
  • 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间
  • ⼀个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量,不存储成员函数。可以理解为:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据实例化出的对象分配物理内存存储数据
代码语言:javascript
复制
#include<iostream>
using namespace std;

class Stack
{
public:
	void Init(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		_top = n;
		_capacity = n;
	}
private:
    // 这里只是声明并没有开空间
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
    // Stack实例化出对象st1和st2
    // 实例化对象时,才会开辟空间
	Stack st1;
	Stack st2;

	st1.Init();
	st2.Init();

	return 0;
}

2.2 对象的大小

类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,但是不包含成员函数。首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么如果对象中非要存储的话,只能是成员函数的指针。但是,如果类实例化多个对象,那么成员函数指针就重复存储多次,太浪费了,所以函数的指针也是不需要存储的。成员函数在被调用时会在编译(有函数的定义)或链接(函数声明和定义分离)的过程中找到函数的地址。总之,成员函数和成员函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。

综上,对象中值存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。

内对齐规则:

  • 第⼀个成员在与结构体偏移量为0的地址处。
  • 其他成员变量要对⻬到某个数字(对齐数)的整数倍的地址处。
  • 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
  • VS中默认的对齐数为8
  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最⼤对⻬数的整数倍处,结构体的整体大小
  • 就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
代码语言:javascript
复制
// 计算A/B/C实例化对象的大小
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;//8
	// 开1byte是为了占位,不存储实际数据,表示对象存在过
	cout << sizeof(B) << endl;//1
	cout << sizeof(C) << endl;//1

	B b1;
	B b2;
	cout << &b1 << endl;
	cout << &b2 << endl;

	C c1;
	C c2;
	cout << &c1 << endl;
	cout << &c2 << endl;

	return 0;
}

没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个 字节呢?因为如果⼀个字节都不给,无法表示对象存在过,所以这里给1字节,纯粹是为了占位标识对象存在。

三. this指针

Data类中有 Init 和 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 和 Print 函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了 一个隐含的this指针解决这里的问题

编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。比如Date类的Init的真实原型为, void Init(Date* const this, int year, int month, int day)

类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this- >_year = year

C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用 this 指针。

代码语言:javascript
复制
class Date
{
public:
	// void Init(Date* const this, int year, int month, int day)
	// this指针在函数体内部可以显示使用,但是不建议
    void Init(int year, int month, int day)
	{
		this->_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	
    // this指针在形参和实参中不能显示使用(编译时编译器会处理),但是在函数体内部可以显示使用
	// d1.Init(&d1, 2025, 10, 1);
	d1.Init(2025, 10, 1);

	// d1.Print(&d1);
	d1.Print();
	return 0;
}

分析一下代码能否正常运行?

代码1:

代码语言:javascript
复制
class A
{
public:
	void Print()
	{
		cout << "A::print()" << endl;
	}
private:
	int _a1;
};

// 情况1:
int main()
{
    // 调用成员变量或成员函数,可以用对象或对象的指针访问
	A* ptr = nullptr;
	ptr->Print();
    // 这里的 -> 表示的是调用成员函数,而不是解引用,通过一个空指针调用成员函数Print
    // -> 只是表示调用这个成员函数,call 函数地址,而print函数(print地址编译成指令时)并没有存到p指向的对象里面(也就不存在解引用)
    // 因为函数地址在编译或链接时就已经确定了,所以就不会通过解引用去调用这个函数

	return 0;
}
// 正常运行
// ptr->Print(); 这句代码整体表示的含义就是调用A类中的Print()函数,ptr是作为参数传递给this指针                                      
// 而在Print()函数内部不存在对this指针的解引用,所以能够正常运行
// 实际上被编译器处理为
A::Print(ptr);  // 传递ptr作为this指针

// 情况2:
int main()
{
    // 调用成员变量或成员函数,可以用对象或对象的指针访问
	A* ptr = nullptr;
	(*ptr).Print(); // 这句代码表示的含义与ptr->Print();是一样的,都是调用A类中的Print()函数
    // 但是这个函数的地址并没有存在这个对象里面,所以就不会通过解引用去调用这个函数
    // ptr是作为参数传递给this指针,因为函数体内部不存在对this指针的解引用,所以能正常运行

	return 0;
}
// 正常运行

代码2:

代码语言:javascript
复制
class A
{
public:
	void Print()
	{
		cout << _a1 << endl;
        // 要访问成员变量_a1,存在对this指针的解引用
		cout << "A::print()" << endl;
	}
private:
	int _a1;
};

int main()
{
	A* ptr = nullptr;
	(*ptr).Print();

	return 0;
}

// 不能正常运行

结语

如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 类的定义
    • 1.1 类定义的格式
    • 1.2 访问限定符
    • 1.3 类域
  • 二. 实例化
    • 2.1 实例化的概念
    • 2.2 对象的大小
  • 三. this指针
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档