文章目录
C++类与对象------------详细入门指南
前言
1. 类的定义
1.1 类定义的基本格式
示例代码
解释
1.2 访问限定符
示例代码
解释
1.3 类域
示例代码
解释
1.4 成员命名规范
常见的命名规定:
实例:
扩展:
1.5 class与struct的默认访问权限
示例:
2. 类的实例化
2.1类实例化的概念
示例代码
解释
2.2 对象的大小与存储
示例代码
解释
2.3 对象大小:空类的情况
空类对象的大小
为什么空类对象占1字节?
示例代码:空类的对象大小
输出结果:
解释:
括展:含有静态成员的类
结论:
3. this指针
3.1 什么是this指针
this指针的内部机制
3.2 示例代码
解释
括展解释:this指针使用的情况?
3.3 this指针的测试题
测试题1:this指针的基本行为
问题描述:
解释:
正确答案 C.正常运行
测试题2:this指针与成员变量的访问
问题描述:
解释:
正确答案: B.运行崩溃
详细解释:为什么不是空指针访问
测试题3:this指针的存储位置
4. C++和C语言实现Stack的对比
4.1 C语言实现Stack
C语言实现Stack的代码示例
C语言实现Stack的详细解释
4.2 C++实现Stack
C++实现Stack的代码示例
C++实现Stack的详细解释
扩展解释:构造函数和析构函数
4.3 C++和C语言实现Stack的对比总结
5. 内存对齐
5.1 内存对齐规则
5.2 示例代码
解释
扩展解释:为什么存在内存对齐?
5.3 内存对齐的影响
解释
C++类与对象------------详细入门指南
前言
欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
点赞、收藏与分享:觉得这篇文章对你有帮助!别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!
1.类的定义
在C++中,类是面向对象编程的基础概念之一。类通过将数据和行为封装在一起,模拟现实世界中的实体。通过类,我们可以定义对象的属性(成员变量)和行为(成员函数)。
1.1 类定义的基本格式
类的定义使用class关键字,后面跟上类的名称。在C++中,类体需要用大括号{}包裹,并在类定义结束时加上分号;。类中的内容为类的成员,包括:
. 成员变量: 即类的属性:存储类的状态。
**. 成员函数:**即类的方法:定义类的行为。
示例代码
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
// ...扩容
array[top++] = x;
}
int Top()
{
assert(top > 0);
return array[top - 1];
}
void Destroy()
{
free(array);
array = nullptr;
top = capacity = 0;
}
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
}; // 分号不能省略
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
解释
. class: 定义类的关键字
. Stack: 类的名称
1.2 访问限定符
访问限定符是C++实现封装的方式之一,它决定了类的成员能否被类外部的用户访问。
• public:修饰的成员在类外可以直接被访问
• private:修饰的成员只能在类内部访问,类外不能直接访问。
• protected:类似于private,但在继承中,子类可以直接访问protected成员。
访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 },即类结束。
示例代码
class Date {
public:
// 公有成员函数
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
// 私有成员变量
int _year;
int _month;
int _day;
};
int main() {
Date d;
d.Init(2024, 3, 31);
return 0;
}
解释
public:声明的 Init 函数是公共的,可以在类外部使用。
private:_year、_month、和_day是私有成员,不能在类外部直接访问,只有类的成员函数能访问这些变量。
1.3 类域
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作
⽤域操作符指明成员属于哪个类域。
#include <iostream>
using namespace std;
class Stack {
public:
void Init(int n = 4);
private:
int* array;
size_t capacity;
size_t top;
};
// 使用作用域解析符在类外定义成员函数
void Stack::Init(int n) {
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array) {
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
int main() {
Stack st;
st.Init();
return 0;
}
解释
Stack::Init: ::作用域解析符表示Init函数属于Stack类。通过作用域解析符,编译器可以知道该函数属于哪个类,并可以在类的作用域中查找成员变量array、capacity、和top。
1.4 成员命名规范
在C++中,通常会为类的成员变量使用特定的命名约定,以避免与函数参数或局部变量混淆。这些命名约定可以提高代码的可读性和维护性。
常见的命名约定:
使用下划线 :在成员变量的名称前加一个下划线,以区分成员变量和其他变量。
使用 m 前缀:成员变量以 m_ 开头,表示 "member"(成员变量)。
驼峰命名法:使用驼峰命名法,例如 myVariable,使得代码更易读。
class Date {
private:
int _year; // 使用下划线前缀
int m_month; // 使用 m_ 前缀
int dayOfMonth; // 使用驼峰命名法
public:
void SetDate(int year, int month, int day) {
_year = year;
m_month = month;
dayOfMonth = day;
}
};
括展:
这种命名约定并不是C++语言的强制要求,而是遵循公司或团队的编码风格指南。使用这些命名约定可以避免命名冲突,并让代码的意图更加明确。例如,在 SetDate
函数中,使用 _year
和 year
可以轻松区分成员变量和函数参数,减少混淆。
1.5 class和struct的默认访问权限
在C++中,class
和 struct
的区别主要在于默认的访问权限:
- 在
class
中,未标明的成员变量和成员函数默认是private
。 - 在
struct
中,未标明的成员变量和成员函数默认是public
。
struct ExampleStruct {
int a; // 默认 public
};
class ExampleClass {
int b; // 默认 private
};
2. 类的实例化
2.1实例化概念
实例化是指在物理内存中创建对象的过程。类是对象的抽象描述,它提供了对象的结构和行为,但是类本身并不占用物理空间,只有当类被实例化时,才会在内存中分配空间。
示例代码
#include <iostream>
using namespace std;
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;
d1.Init(2024, 3, 31);
d1.Print();
Date d2;
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
解释
- Date d1 :实例化一个
Date
对象d1
。 - Init :调用
Init
函数初始化d1
对象的成员变量_year
、_month
和_day
。 - Print:输出对象的日期信息。
2.2 对象的大小与存储
当类被实例化为对象时,对象的大小取决于类的成员变量 。成员变量需要分配空间,但成员函数不会在对象中存储。成员函数是存储在单独的代码段中的一段指令。
示例代码
#include <iostream>
using namespace std;
class A {
private:
char _ch; // 1 字节
int _i; // 4 字节
};
int main() {
A a;
cout << sizeof(a) << endl; // 输出对象的大小
return 0;
}
解释
- 该类的成员变量
_ch
和_i
总共占用 5 字节,但由于内存对齐,实际对象的大小可能是 8 字节。内存对齐规则保证了访问效率(见下文解释)。
2.3 对象大小:空类情况
在C++中,对象的大小 是由类的成员变量决定的,而类的成员函数不会影响对象的大小。因此,如果一个类没有任何成员变量,而只有成员函数,我们称之为空类。这种空类的对象大小在C++中也是有规定的。
空类对象大小
尽管空类没有成员变量,但在C++中,空类的对象大小仍然不是零。空类的对象大小是1字节。这是因为C++规定每个类的对象都必须占有唯一的地址,即使类中没有成员变量。这1字节的大小用于确保不同的对象在内存中拥有唯一的地址。
为什么空类对象占1个字节?
即使类中没有任何成员变量,C++仍然需要给这个对象分配空间,以便它在内存中有一个唯一的地址。这样做有几个目的:
**1.区分不同对象的地址:**如果类对象占据0字节,那么多个对象可能会共享同一个内存地址,这会导致无法区分不同的对象。因此,C++规定空类对象至少占用1字节的空间。
**2.确保类的指针行为一致:**即使是空类的指针,指向不同对象时,它们也必须指向不同的内存地址。如果空类对象占0字节,就无法保证这一点。
示例代码
#include<iostream>
using namespace std;
class EmptyClass {
public:
void Print() {
cout << "This is an empty class!" << endl;
}
};
int main() {
EmptyClass e1, e2;
cout << "EmptyClass 对象 e1 的大小: " << sizeof(e1) << " 字节" << endl;
cout << "EmptyClass 对象 e2 的大小: " << sizeof(e2) << " 字节" << endl;
return 0;
}
输出结果 :
EmptyClass 对象 e1 的大小: 1 字节
EmptyClass 对象 e2 的大小: 1 字节
解释:
- 尽管
EmptyClass
中没有任何成员变量,但每个对象e1
和e2
仍然占用 1 字节。这是为了保证每个对象都有唯一的内存地址,C++通过分配1字节来实现这一点。
括展:含有静态成员类
如果一个类只包含静态成员函数或静态成员变量,该类的对象大小依然是1字节。原因是静态成员属于整个类,而不是某个具体的对象,因此静态成员(无论是函数还是变量)不会影响对象的大小。
示例代码
#include<iostream>
using namespace std;
class StaticClass {
public:
static void StaticPrint() {
cout << "This is a static function!" << endl;
}
static int staticVar; // 静态成员变量
};
// 定义并初始化静态成员变量
int StaticClass::staticVar = 10;
int main() {
StaticClass s1;
StaticClass s2;
cout << "StaticClass 对象 s1 的大小: " << sizeof(s1) << " 字节" << endl;
cout << "StaticClass 对象 s2 的大小: " << sizeof(s2) << " 字节" << endl;
// 修改静态成员变量
s1.staticVar = 20;
cout << "s1.staticVar: " << s1.staticVar << endl;
cout << "s2.staticVar: " << s2.staticVar << endl;
return 0;
}
输出结果:
StaticClass 对象 s1 的大小: 1 字节
StaticClass 对象 s2 的大小: 1 字节
s1.staticVar: 20
s2.staticVar: 20
解释:
静态成员函数和静态成员变量都属于类,而不是对象 。因此,类 StaticClass
中的静态成员函数 StaticPrint
和静态成员变量 staticVar
都不影响对象的大小。
对象 s1
和 s2
的大小依然为 1 字节,静态成员变量在类的所有对象中共享,只占用一份内存。
结论:
- 空类对象的大小是1字节,即使类中没有成员变量,C++仍然会为每个空类对象分配1字节的空间,以确保每个对象有唯一的内存地址。
- 静态成员不影响对象的大小,因为它们属于类,而不是对象,空类或仅含有静态成员函数或者变量的类对象同样占用1字节。
---------------------------------------------------------------------------------------------
3. this指针
3.1什么是this指针
this
是C++中的一个隐式指针,存在于每一个非静态成员函数中。this
指针指向调用该成员函数的当前对象。在类的成员函数中,this
指针可以用来访问类的成员变量和成员函数。
- 当我们在成员函数中使用类的成员变量时,本质上是通过
this
指针访问该成员变量。 this
指针是隐含的,C++自动将它传递给所有非静态成员函数。
this指针内部机制
在C++中,当类的成员函数被调用时,this
指针会被自动传递给该函数。this
指针的类型是 const Type* const
,它是一个只读指针,指向当前对象。你不能修改 this
指针的值,但可以通过 this
访问对象的成员。
3.2 示例代码
#include<iostream>
using namespace std;
class Date {
public:
void Init(int year, int month, int day) {
this->_year = year; // 通过 this 指针访问成员变量
this->_month = month;
this->_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Init(2024, 3, 31);
d1.Print();
return 0;
}
解释:
this :在 Init
函数中,this->_year = year
表示将传入的参数 year
赋值给当前对象的 _year
成员变量。this
指针指向当前调用 Init
函数的对象(即 d1
)。
拓展解释:this指针使用的情况?
在通常的情况下,编写代码时我们不需要显式使用 this 指针,因为C++会自动处理类的成员和参数之间的冲突。但是在某些情况下,例如当函数的参数名称和成员变量名称相同时,使用 this 指针可以明确地表示成员变量。
void Init(int year, int month, int day) {
this->year = year; // 通过 this 指针区分成员变量和函数参数
this->month = month;
this->day = day;
}
在上面的代码中,如果不使用 this
,编译器会把 year
解释为函数参数,而不是成员变量 的year
。这就导致了未定义的行为,成员变量没有被正确的赋值。
所以也进一步说明了我们在类里面定义的成员变量需要有一种命名规范,笔者习惯用
_
作为前缀。
3.3 this指针测试题
测试题1:this指针基本的行为
#include<iostream>
using namespace std;
class A {
public:
void Print() {
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // 能否正常运行?
return 0;
}
问题描述:
我们定义了一个 A
类,包含一个成员函数 Print()
,它只是输出一条字符串。我们通过空指针 p
调用 Print()
函数,询问这段代码能否正常运行。
解析:
- 在
main()
函数中,A* p = nullptr;
创建了一个指向A
类的空指针p
。 - 随后我们通过空指针
p->Print()
来调用成员函数Print()
,这里的this
指针其实是空指针。
为什么不会报错?
当我们调用 p->Print() 时,this 指针实际上等于 nullptr,但由于 Print() 函数没有访问任何成员变量,因此C++允许这个调用。
this 指针是隐含的,虽然在函数内部会传递 this,但是如果成员函数不访问任何成员变量,C++不需要解引用这个空指针,因此不会出现空指针访问的错误。
Print() 函数只是输出了一段固定的文本,不涉及对象的状态或成员变量,因此即使 this 是空指针,也不会导致问题。
正确答案:C. 正常运行
测试题2:this指针与成员变量的访问
#include<iostream>
using namespace std;
class A {
public:
void Print() {
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // 能否正常运行?
return 0;
}
问题描述:
这段代码和前一个问题类似,但在 Print() 函数中多了一个成员变量 _a 的访问。通过空指针 p 调用 Print(),是否会出现空指针访问的问题?
解析:
在 main() 中,和之前一样,A* p = nullptr; 创建了一个指向 A 类的空指针 p。
当调用 p->Print() 时,this 指针仍然是 nullptr。
为什么会崩溃?
在 Print() 函数中,除了输出 A::Print() 以外,代码还试图访问类的成员变量 _a。
当 this 指针为 nullptr 时,访问 this->_a 等同于尝试通过空指针访问成员变量。这是一种未定义行为,在大多数系统中会导致程序崩溃。
成员变量 _a 存储在对象的内存空间中,而通过空指针访问成员变量时,由于没有实际的对象空间可用,因此程序在运行时会发生崩溃。
正确答案:B. 运行时崩溃
详细解释:为什么不是空指针访问
在这两个测试题中,关键点在于是否访问了成员变量:
测试题 1 中,Print() 函数没有访问成员变量,所以即使 this 是空指针,C++也不会触发空指针访问错误。这是因为成员函数本质上只是一个在内存中的函数指针,调用它并不一定需要访问实际对象的内存。
测试题 2 中,Print() 函数试图访问成员变量 _a。由于成员变量存储在对象的内存空间中,而空指针 this 并没有指向有效的内存区域,所以在运行时会试图通过空指针访问内存,导致程序崩溃。这是典型的空指针访问错误。
总结来说,空指针调用成员函数本身并不会报错,因为成员函数本来就不在类中,所以不是解引用,编译时的汇编代码这里就只是一段函数的地址而已,只是这里没有对象,传过去的this指针就是空指针,但只要该成员函数不涉及访问成员变量或其他依赖对象内存的操作那就不会报错。然而,一旦成员函数试图通过 this 指针访问成员变量,程序就会崩溃,因为 this 为 nullptr,没有有效的内存空间可供访问。
测试题 3:this 指针的存储位置
选择题:this 指针存在于内存的哪个区域?
A. 栈
B. 堆
C. 静态区
D. 常量区
E. 对象里
正确答案:A. 栈
解释:
this 指针作为成员函数的一个隐含参数,存储在栈中。每当一个成员函数被调用时,this 指针会作为函数参数被压入栈中。
this指针其实就是函数的参数而已。
4. C++和C语言实现Stack对比
4.1 C语言实现Stack
在C语言中,实现 Stack 需要使用 struct 来定义栈的数据结构,并且所有的栈操作(如初始化、压栈、弹栈等)都通过独立的函数实现。由于C语言不支持面向对象编程,数据和操作必须分开处理,所有栈操作的函数需要手动传递 Stack 结构体的指针作为参数。
C语言实现Stack的代码示例
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack {
STDataType* array;
int top;
int capacity;
} Stack;
void Init(Stack* s) {
s->array = (STDataType*)malloc(4 * sizeof(STDataType));
s->capacity = 4;
s->top = 0;
}
void Destroy(Stack* s) {
free(s->array);
s->array = NULL;
s->top = 0;
s->capacity = 0;
}
void Push(Stack* s, STDataType x) {
if (s->top == s->capacity) {
s->capacity *= 2;
s->array = (STDataType*)realloc(s->array, s->capacity * sizeof(STDataType));
}
s->array[s->top++] = x;
}
STDataType Top(Stack* s) {
assert(s->top > 0);
return s->array[s->top - 1];
}
void Pop(Stack* s) {
assert(s->top > 0);
--s->top;
}
bool Empty(Stack* s) {
return s->top == 0;
}
int main() {
Stack s;
Init(&s);
Push(&s, 1);
Push(&s, 2);
printf("栈顶元素: %d\n", Top(&s));
Pop(&s);
printf("栈顶元素: %d\n", Top(&s));
Destroy(&s);
return 0;
}
C语言实现Stack的详细解释
结构体Stack:这是一个结构体,包含了三个成员:
array:一个指向栈的动态数组的指针,用来存储栈中的元素。
top:指向栈顶元素的指针,它代表当前栈中元素的个数。
capacity:栈的容量,表示栈中最多可以容纳的元素个数。
函数Init:用于初始化栈的大小,并为数组分配内存。C语言中没有构造函数,因此必须通过函数显式初始化结构体。
函数Push:将元素压入栈中,如果栈满则进行扩容操作,使用 realloc 函数为栈分配更大的内存。
函数Top:返回栈顶元素,调用时需要确保栈不为空。
函数Pop:将栈顶元素弹出,减少 top 的值。
内存管理:通过 malloc 和 realloc 动态分配内存,使用 free 释放内存。
4.2 C++语言实现Stack
C++通过类的封装,可以将数据和操作放在一起。栈的实现不仅更为简洁,而且通过封装性提高了代码的安全性和可维护性。相比C语言,C++不需要手动传递 Stack
指针,而是通过类的成员函数自动操作栈。
C++实现Stack的代码示例
#include<iostream>
using namespace std;
class Stack {
public:
Stack(int n = 4) {
_array = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
~Stack() {
free(_array);
_array = nullptr;
}
void Push(int x) {
if (_top == _capacity) {
_capacity *= 2;
_array = (int*)realloc(_array, _capacity * sizeof(int));
}
_array[_top++] = x;
}
int Top() {
assert(_top > 0);
return _array[_top - 1];
}
void Pop() {
assert(_top > 0);
--_top;
}
bool Empty() {
return _top == 0;
}
private:
int* _array;
size_t _capacity;
size_t _top;
};
int main() {
Stack s;
s.Push(1);
s.Push(2);
cout << "栈顶元素: " << s.Top() << endl;
s.Pop();
cout << "栈顶元素: " << s.Top() << endl;
return 0;
}
C++实现Stack的详细解释
构造函数和析构函数:
构造函数 Stack(int n = 4):用于初始化栈,分配内存并设置栈的容量。这里的 n 是栈的初始大小,默认值为4。构造函数在对象创建时自动调用,确保对象处于有效的状态。
析构函数 ~Stack():当栈对象被销毁时,自动释放动态分配的内存。这是C++相比C语言的一个显著优势,因为不需要手动调用 Destroy 函数来释放资源。
成员函数Push:与C语言中的 Push 函数类似,用于将元素压入栈中。栈满时会自动扩容,但通过成员函数的封装,这一操作对类外的用户是透明的,用户只需要调用 Push 方法即可。
成员函数Top:返回栈顶元素,和C语言一样,操作之前会检查栈是否为空,保证操作的安全性。
封装性:相比C语言,C++通过类的 private 成员变量 _array、_capacity 和 _top,将栈的实现细节封装起来,防止用户直接操作这些数据。所有的操作都通过 public 成员函数完成,保证了数据的安全性。
拓展解释:构造函数和析构函数
(在下一篇博客会详细解释)
构造函数:它是类中的特殊函数,当类的对象被创建时,构造函数会被自动调用,用于初始化对象。在 Stack 类中,构造函数初始化栈的容量,并为数组分配内存。
析构函数:它也是类中的特殊函数,当对象生命周期结束(如对象作用域结束时)时,析构函数会自动调用,用于释放对象所占用的资源。在 Stack 类中,析构函数用于释放 malloc 分配的内存,避免内存泄漏。
4.3 C++和C语言实现Stack的对比总结
通过对C和C++实现 Stack 的对比,可以得出以下几点总结:
封装性:C++通过类的封装将数据和操作整合在一起,类的用户不需要关心栈的实现细节,而C语言的实现则需要用户手动调用函数并管理结构体的状态。
内存管理:C语言中,内存管理是手动的,开发者必须显式调用 free 函数释放内存。而在C++中,析构函数自动负责资源的释放,避免了忘记释放内存导致的内存泄漏问题。
安全性:C++中的类通过 private 关键字保护类的内部数据,防止外部代码随意修改类的成员变量,增强了数据的安全性。而C语言没有这种封装机制,所有数据都可以通过结构体直接访问,容易导致意外的修改和错误。
5. 内存对齐
内存对齐是计算机系统中用于优化数据访问的机制。在C++中,类的成员变量在内存中的存放位置要遵循特定的对齐规则,以提高CPU的访问效率。
可以看这篇文章,嘎嘎详细自定义类型:结构体-CSDN博客
你的支持就是我创作的动力!