什么是类 (Class)?
类 是一个抽象的概念。它定义了一类事物所共有的属性 (数据)和行为(函数),类本身不占用内存空间(静态定义除外),它只是告诉编译器:如果创建一个这种类型的变量,它应该长什么样
-
属性(Member Variables): 描述事物的特征(例如:颜色、尺寸、品牌)
-
行为(Member Functions): 描述事物能做什么(例如:加速、刹车、鸣笛)
什么是对象 (Object)?
对象 是类的具体实例。当你根据"蓝图"(类)真正制造出一个"建筑"时,这个建筑就是一个对象,对象是存在于内存中的,你可以操作它
类的定义
类定义格式
class为定义类的关键字,name为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省 略,类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或 者成员函数
• 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_ 或者 m 开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求
• C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,一般情况下我们还是推荐用class定义类
• 定义在类面的成员函数默认为inline
代码示例
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
if (month > 0 && month <= 12)
{
_year = year;
_month = month;
_day = day;
}
else
{
std::cout << "月份设置非法!" << std::endl;
}
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
访问限定符
C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限 选择性的将其接口提供给外部的用户使用
• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访 问,protected和private是一样的,以后继承章节才能体现出他们的区别
• 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没 有访问限定符,作用域就到 }即类结束
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public
• 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public

类的声明
-
关键字
class:它是定义"类"的起始标志。你可以把Date看作是一个你自己创造的新数据类型 ,就像int或float一样,只不过它更复杂、功能更强 -
大括号与分号 :类的内容包含在
{}之中,注意 :大括号结束后的分号;是语法强制要求的,它代表这个类声明语句的结束
初始化函数Init就在这里给变量初始值 ,这个初始值我们可以自己定义,也可以让编译器随机生成,但生成的这个值是一个随机值
例如
cpp
#include <iostream>
class Date
{
public:
void Init(int year, int month, int day)
{
if (month > 0 && month <= 12)
{
_year = year;
_month = month;
_day = day;
}
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2026, 3, 22);
d1.Print();
return 0;
}

我现在用Date数据类型去定义一个d1变量,这个过程叫做**实例化,**这里我给了一个初始化的日期,那如果我没有给日期会怎么样?
cpp
#include <iostream>
class Date
{
public:
void Init(int year, int month, int day)
{
if (month > 0 && month <= 12)
{
_year = year;
_month = month;
_day = day;
}
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}

如果我们不给初始值,编译器就回生成一个随机指
类域
在 C++ 中,大括号 {} 通常都会开启一个作用域。当你定义一个类时:
cpp
class Date {
// 这里开始就是 Date 的类域
void Init(int year, int month, int day);
int _year;
// 这里类域结束
};
类域决定了编译器寻找成员变量和成员函数的范围
类域的作用
名字隔离(防止冲突)
假设你的程序里有两个类:Student 类和 Teacher 类。它们可能都有一个成员变量叫 _name
-
因为它们分别属于不同的类域,所以互不干扰
-
编译器知道
Student::_name和Teacher::_name是完全不同的东西
域作用域限定符 ::
当你需要在类外面定义成员函数时,类域的概念就变得至关重要了,你必须告诉编译器:"这个函数不是普通的全局函数,它是属于某个特定类域的"
对象大小
分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针,再分析一下,对象中是否有存储指针的必要呢,假如Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量 _year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是一样的,存储在对象中就浪费了,如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外哆嗦一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在 运行时找,就需要存储函数地址

内存对齐规则
这个以前我们再结构体讲过,这里不多赘述https://mp.csdn.net/mp_blog/creation/editor/156221708
this指针
Date类中有 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指针
cpp
#include<iostream>
class calendar
{
public:
//void Init(calendar* const this int year, int month, int day)
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//void print(calendar* const this)
void print()
{
//this->可写可不写
std::cout <<this-> _year << "/" << this->_month << "/" <<this-> _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
calendar day1;
calendar day2;
//函数调用形参和实参不能写this指针
//day1.Init(&day1,2005,3,12);
day1.Init(2005, 3, 12);
//day1.print(&d1);
day1.print();
//day2.Init(&day2,2005,3,12);
day2.Init(2009, 9, 19);
//day2.print(&d2);
day2.print();
return 0;
}
例1
下面程序编译运行结果是()
A、编译报错 B、运行崩溃 C、正常运行
cpp
#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* p = nullptr; 和 p->Print(); 的第一反应通常是:"这一定会导致空指针解引用,程序会崩溃!"
但实际运行结果可能会让你大吃一惊:程序不仅没有崩溃,还成功打印出了 A::Print()
为什么程序没有崩溃?
要理解这个现象,我们需要结合你之前看的那张**"对象存储方式设计"**图来分析:
- 成员函数的存储位置
类成员函数(如 Print)并不存储在对象实例中 ,而是存储在公共代码区。
-
当编译器编译
p->Print()时,它其实并不需要去内存里找对象p的内容。 -
编译器知道
Print函数的地址,它会将这行代码转换成类似A::Print(p)的调用。
this指针的本质
当你通过对象指针调用成员函数时,指针 p 会作为隐藏参数传递给函数,也就是我们常说的 this 指针。
- 在这个例子中,
p是nullptr,所以进入Print函数后,其内部的this指针也是nullptr
- 是否发生了"解引用"?
空指针崩溃的根本原因是尝试访问空地址上的数据。
-
在
Print函数内部,代码只执行了cout << "A::Print()" << endl; -
重点: 这个函数没有访问任何成员变量 (如
_a)。 -
因为没有访问
_a(即没有执行类似this->_a的操作),所以程序根本没有去解引用那个空的this指针。既然没去碰那个空地址,自然也就不会崩溃
例2
下面程序编译运行结果是()
A、编译报错 B、运行崩溃 C、正常运行
cpp
#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;
}
首先说结论:会崩溃
为什么这次一定会崩溃?
我们要从 this 指针 的微观操作来分析。请看这个过程:
-
函数调用阶段 : 当你执行
p->Print()时,编译器依然能找到Print函数的地址(因为它在公共代码区),它把p(也就是nullptr)传给了函数内部的this指针,到这一步,程序还没死 -
执行第一行 :
cout << "A::Print()" << endl;这行代码只涉及常量字符串输出,不依赖任何对象数据。所以你会发现,程序甚至能打印出这一行 -
解引用 :
cout << _a << endl;在编译器眼里,_a其实是this->_a的缩写
- 此时
this的值是0x00000000(空地址)。 - 代码试图去
0x0这个地址寻找变量_a的值。 - 操作系统(Windows/Linux)不允许任何程序访问 0 号地址。为了保护系统安全,系统直接把你的程序"杀掉"了