C++类与对象深度解析:从基础到应用
目录:
- 1. 类的基本概念与定义
- 1.1 类的定义与访问限定符
- 1.2 实例化与对象内存布局
- 1.3 this指针
- 2.类的默认成员函数
- 2.1 构造函数与析构函数详解
- 构造函数
- 析构函数
- 2.2 拷贝构造函数
- 2.3 赋值运算符重载
- 运算符重载
- 3. 初始化列表
- 4.类型转换
- 5. 静态成员
- 6.友元和内部类
- 6.1 友元
- 6.2 内部类
- 总结
1. 类的基本概念与定义
1.1 类的定义与访问限定符
类是C++封装数据与行为的核心单元,通过class
或struct
定义:
class Stack
{
public: // 公有成员,外部可访问void Push(int x);
private: // 私有成员,仅类内访问int* _array; // 成员变量习惯加前缀_或msize_t _top;
};
- 访问限定符:
public
:类外可直接访问。private
/protected
:类外不可直接访问(默认class
为private
,struct
为public
)。
1.2 实例化与对象内存布局
实例化即分配内存(满足内存对齐)创建对象,对象大小仅包含成员变量(成员函数存于代码段):
Stack st; // 实例化对象
cout << sizeof(st); // 输出对象大小(不含函数)
- 空类大小:空类对象占1字节,标识存在性。
为什么不算成员函数呢?
每一个对象都是独立的个体,有自己的私有属性(身高、体重、大小…),正是这些私有属性所以每个对象都是独一无二的不是吗?而成员函数除了传的参数不一样,实现功能都是一样的,如果每一个对象里面都包含功能一样的函数是不是有点浪费空间了呢,因此成员函数都是放在公共的空间里的,不在对象的空间里
成员函数声明和定义可以分开写
class A
{public:void print();//声明private:}void A::print()
{}
形象的理解:类就是个蓝图,对象就是根据蓝图创建出来的具体的房子
1.3 this指针
class Date
{
public:void print(){cout << _year <<"/"<<_month<<"/"<<_day << endl;}private:int _year;int _month;int _day;
};
假设这里有两个Date类型的对象d1、d2,当我们去调用print这个成员函数时,它怎么知道我们要访问的是那个对象里成员变量?
- 隐含参数:定义在类里的成员函数都隐藏了一个
this指针
,实际接收ClassName* const this
参数,C++规定不能在实参和形参的位置显示的写this指针
(编译时编译器会处理),但是可以在函数体内显示使⽤this指针
。 - 访问成员:
_year
等价于this->_year
。
class Date
{
public://void print(Date* const this)void print(){cout << _year <<"/"<<_month<<"/"<<_day << endl;// cout <<this-> _year <<"/"<<tjos->_month<<"/"<<this->_day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.print();//实际上为d1.print(&d1);
}
2.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动动生成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。(C++11后还有两个)
2.1 构造函数与析构函数详解
构造函数
-
作用:替代
Init
函数,初始化对象(非分配内存)。 -
特点:
- 与类同名、无返回值、支持重载。
- 默认构造函数:用户未定义时编译器生成(对内置类型不初始化或者初始化为0,不同的编译器不同;对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表)。 默认构造函数包括:无参的,全缺省的,编译器自动生成的,三种只能存在一种,总结⼀下就是不传实参就可以调用的构造就叫默认构造
- 对象实例化后,系统会自动调用对应的构造函数
:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,
如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
class Date {
public:Date() // 无参构造{_year = 2024; } Date(int year = 2024, int month = 4, int day = 26)//全缺省{_year = year;_month = month;_day = day;}//编译器自动生成......Date(int year, int month, int day) // 带参构造{ _year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year, _month, _day;
};int main()
{
// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
// 编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤Date d1; // 调⽤默认构造函数Date d2(2025, 1, 1); // 调⽤带参的构造函数
// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)Date d3();d1.Print();d2.Print();
return 0;
}
析构函数
- 作用:清理资源(如释放堆内存)。
- 特点:
- 名为
~ClassName
,无参数、不可重载。若未显式定义,系统会自动生成默认的析构函数(编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数) - 对象生命周期结束时自动调用(如局部对象离开作用域)。
- 名为
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{}*/
private:Stack pushst;Stack popst;
};
int main()
{Stack st;MyQueue mq;return 0;
}
2.2 拷贝构造函数
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
- 场景:值传递自定义类型参数时,值返回自定义类型对象时
- 特点:
- 构造函数的重载形式,参数必须为本类对象的引用(可搭配其他带默认值的参数)
- 未显式定义时,编译器自动生成:
- 内置类型 → 浅拷贝(值复制)
- 自定义类型 → 调用其拷贝构造
有时候浅拷贝可以完成我们的需求,但是如果是Stack这种不显示写,会自动实现浅拷贝,而导致一些错误
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}//显示写拷贝构造Stack(const Stack& st){// 需要对_a指向资源创建同样⼤的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;
}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};
int main()
{Stack st1;// Stack不显⽰实现拷贝构造,⽤⾃动⽣成的拷贝构造完成浅拷贝// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃Stack st2 = st1;MyQueue mq1;// MyQueue⾃动⽣成的拷贝构造,会⾃动调⽤Stack拷贝构造完成pushst/popst// 的拷贝,只要Stack拷贝构造⾃⼰实现了深拷贝,他就没问题MyQueue mq2 = mq1;return 0;
}
为什么第一个参数要用引用呢?
注意:Stack st2(st1) ;和Stack st2 = st1;这样写本质是一样的,看下面的汇编代码
使用建议:
- 无资源管理(如Date类)→ 直接用自动生成的
- 有动态资源(如Stack类数组)→ 必须显式深拷贝
- 含自定义类型成员(如MyQueue含Stack)→ 自动调用其拷贝构造
- 显式写了析构函数释放资源 → 必须显式实现拷贝构造
2.3 赋值运算符重载
- 场景:作用于两个已存在的对象,完成内容复制(拷贝构造是初始化新对象)
- 特点
-
语法特点 :
- 必须定义为成员函数,参数为
const 类名&
(建议这样写,使用引用避免传值传参会有拷贝,带默认值需用const
修饰) - 返回值建议类引用:支持连续赋值(如
a = b = c
),提高效率
- 必须定义为成员函数,参数为
-
未显式定义时,编译器自动生成:
- 内置类型 → 浅拷贝(逐字节复制)
- 自定义类型 → 调用其赋值运算符重载
-
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){cout << " Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}
// 传引⽤返回减少拷贝
// d1 = d2;Date& operator=(const Date& d){// 不要检查⾃⼰给⾃⼰赋值的情况if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*thisreturn *this;
}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2024, 7, 5);Date d2(d1);Date d3(2024, 7, 6);d1 = d3;// 需要注意这⾥是拷贝构造,不是赋值重载// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷贝赋值// ⽽拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象
Date d4 = d1;
return 0;
}
使用建议
- 无需显式实现:
- 成员全为内置类型且无资源管理(如
Date
类) - 含自定义类型成员(如
MyQueue
含Stack
)→ 自动调用其赋值重载
- 成员全为内置类型且无资源管理(如
- 必须显式深拷贝:
- 有动态资源(如
Stack
类数组) - 若显式写了析构函数 → 必须配套自定义赋值重载(避免内存泄漏)
- 有动态资源(如
与拷贝构造区别
- 拷贝构造:对象初始化时由另一个对象创建
- 赋值重载:两个已存在对象赋值时覆盖旧值
总结:处理对象赋值用 operator=
,默认浅拷贝,深资源需手动重载,返回值用引用效率高。
Stack& operator=(const Stack& st) {if (this != &st) {free(_array); // 释放旧资源_array = (int*)malloc(/*...*/); // 深拷贝新资源// 其他成员赋值...}return *this; // 支持连续赋值
}
运算符重载
当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。
例如:我们前面讲过的<<
和>>
运算符,在C语言中是移位运算符,而在C++中可以作为输出和输入使用。
运算符重载函数跟普通函数一样有参数、返回值类型、函数体,不同的是由关键字operator
和其后面要定义的运算符共同组成
返回值类型 operator运算符(参数)
{.............
}
- 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符(+、-、!、++、–、&……)有一个参数;二元运算符(+、-、%、*……)有两个参数,对于二元运算符,左侧的运算对象会传递给第一个参数,右侧的运算对象会传递给第二个参数。
当一个运算符重载函数作为成员函数时,左侧的运算对象会传递给隐藏的
this指针
,也就是说作为成员函数时,运算符重载函数显示的参数数量比运算对象的数量少一个
- 运算符重载函数要么是类的成员,要么至少有一个类类型的参数,对于内置类型的运算对象重载没有意义
- 只能重载已有的运算符,无权创造新的运算符。以下几个运算符不可以重载:
::、.*、.、?:
- 重载运算符的优先级与结合性与对应的内置运算符保持一致
使用:
int main()
{Date d1;Date d2;非成员函数的等价调用d1 + d2;operator+(d1,d2);成员函数的等价调用d1 + d2;d1.operator(d2);}
以下是一个日期类的实现:
#include <iostream>
#include <assert.h>
using namespace std;class Date
{
public:Date(int year = 2025, int month = 4, int day = 24);/*Date():_year(2025),_month(4),_day(27){}*/void print(){cout << _year << "/" << _month << "/" << _day << endl;}//日期比较bool operator<(const Date& d)const;bool operator==(const Date& d)const;bool operator<=(const Date& d)const;bool operator>(const Date& d)const;bool operator>=(const Date& d)const;bool operator!=(const Date& d)const;int GetMonthDays(int year, int month)const{assert(month > 0 && month < 13);static int daysArry[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//判断闰年if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return daysArry[month];}}//日期+天数->日期Date& operator+=(int day);Date operator+(int day)const;//日期-天数->日期Date& operator-=(int day);Date operator-(int day)const;//日期-日期->天数int operator-(const Date& d)constprivate:int _year = 2025;int _month = 4;int _day = 26;
};Date::Date(int year , int month, int day )
{if (month > 0 && month < 13 && day > 0 && day <= GetMonthDays(year, month)){_year = year;_month = month;_day = day;}else{cout << "日期非法" << endl;}
}bool Date::operator<(const Date& d)const
{if (_year < d._year){return true;}else if ((_year == d._year) && (_month < d._month)){return true;}else if((_year == d._year) && (_month == d._month)&&(_day < d._day)){return true;}return false;
}bool Date::operator==(const Date& d)const
{return _year == d._year && _month == d._month && _day == d._day;
}//复用
bool Date::operator<=(const Date& d)const
{return *this < d || *this == d;
}
bool Date::operator>(const Date& d)const
{return !(*this <= d);
}
bool Date::operator>=(const Date& d)const
{return !(*this < d);
}
bool Date::operator!=(const Date& d)const
{return !(*this == d);
}Date& Date::operator+=(int day)
{if (day < 0){return *this -= (-day); // 负数转为减法}_day += day;while (_day > GetMonthDays(_year, _month)){_day -= GetMonthDays(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this;
}Date Date::operator+(int day)const
{Date tmp(*this);tmp += day;return tmp;
}Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day); // 负数转为加法}_day -= day;while (_day <= 0){_month--;if (_month == 0){_month = 12;_year--;}_day += GetMonthDays(_year, _month);}return *this;
}Date Date::operator-(int day)const
{Date tmp(*this);tmp -= day;return tmp;
}int Date::operator-(const Date& d)const
{int flag = 1;Date max = (*this);Date min = d;if (max < min){max = d;min = (*this);flag = -1;}int n = 0;while (min != max){++min;++n;}return n * flag;
}
const成员函数
- 作用:承诺不修改对象状态。
- 语法:
void Print() const { ... }
。
跟前面引用一样,如果用const
修饰一个对象,也会有访问权限放大的问题,并且作为成员函数,this指针
是隐藏的不准在函数参数显示写,而且默认是没有被const
修饰的,于是我们的祖师爷就发明了在后面加const
的语法,代表修饰的是this指针
3. 初始化列表
- 构造函数除了用函数体赋值还可以使用初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式。
- 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
- 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
- 初始化列表总结:
- 无论是否显示写初始化列表,每个构造函数都有初始化列表;
- 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
#include<iostream>
using namespace std;
class Time
{
public:Time(int hour):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};
class Date
{
public:Date(int& x, int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day),_t(12),_ref(x),_n(1){// error C2512: “Time”: 没有合适的默认构造函数可⽤// error C2530 : “Date::_ref” : 必须初始化引⽤// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象}
void Print() const{cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;Time _t; // 没有默认构造int& _ref; // 引⽤const int _n; // const
};
int main()
{int i = 0;Date d1(i);d1.Print();return 0;
}
#include<iostream>
using namespace std;
class Time
{
public:Time(int hour):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};class Date
{
public:Date():_month(2){cout << "Date()" << endl;}void Print() const{cout << _year << "-" << _month << "-" << _day << endl;}
private:// 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的// 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化int _year = 1;int _month = 1;int _day;Time _t = 1;const int _n = 1;int* _ptr = (int*)malloc(12);
};int main()
{Date d1;d1.Print();return 0;
}
4.类型转换
- C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
- 构造函数前面加explicit就不再支持隐式类型转换。
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
#include<iostream>
using namespace std;
class A
{
public:// 构造函数explicit就不再⽀持隐式类型转换// explicit A(int a1)A(int a1):_a1(a1){}//explicit A(int a1, int a2)A(int a1, int a2):_a1(a1), _a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}
private:int _a1 = 1;int _a2 = 2;
};class B
{
public:B(const A& a):_b(a.Get()){}
private:int _b = 0;
};
int main()
{// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3// 编译器遇到连续构造+拷⻉构造->优化为直接构造A aa1 = 1;aa1.Print();const A& aa2 = 1;// C++11之后才⽀持多参数转化A aa3 = { 2,2 };// aa3隐式类型转换为b对象// 原理跟上⾯类似B b = aa3;const B& rb = aa3;return 0;
}
5. 静态成员
- 静态变量:类内声明,类外初始化,所有对象共享。
class Counter
{
public:static int count; // 声明
};
int Counter::count = 0; // 初始化(不受访问限定符限制)
- 静态函数:无
this
指针,仅能访问静态成员。 (非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。)
static int GetCount() { return count; }
- 突破类域就可以访问静态成员,可以通过类名::静态成员 或者对象.静态成员 来访问静态成员变量和静态成员函数。(静态成员也是类的成员,受public、protected、private 访问限定符的限制。)
// 实现⼀个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++_scount;}~A(){--_scount;}static int GetACount(){return _scount;}
private:// 类⾥⾯声明static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;int main()
{cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;cout << a1.GetACount() << endl;// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)//cout << A::_scount << endl;return 0;
}
6.友元和内部类
6.1 友元
-
定义:用
friend
声明,可访问类的私有/保护成员,突破封装(慎用) -
分类:
- 友元函数:普通函数声明为类友元
class Date {friend void PrintDate(const Date& d); // 声明友元
private:int _year;
};
void PrintDate(const Date& d) {cout << d._year; // 访问私有成员
}
- 友元类:整个类声明为另一个类的友元
//class B可访问class A的所有成员
class A {friend class B; // B是A的友元类
private:int _secret;
-
友元函数特点:
- 非成员函数,但能访问类私有数据
- 声明位置不限(类内任意处)
- 支持多类共享(可作多个类的友元)
-
友元类特点:
- 友元类的所有成员函数均可访问目标类私有数据
- 单向关系(A是B友元 ≠ B是A友元)
- 不可传递(A友元的友元 ≠ A的友元)
友元是C++开后门的钥匙,用friend
声明可跨权限访问,便利但破坏封装,慎用为妙。
6.2 内部类
-
本质
- 嵌套封装:定义在另一个类内部的独立类,本质是类的逻辑封装手段。
-
特点
- 受外部类作用域和访问权限(如
private/protected
)限制 - 外部类对象不包含内部类对象(独立存在)
- 受外部类作用域和访问权限(如
-
访问关系
- 内部类默认是外部类的友元类 → 可访问外部类私有成员
- 外部类不能直接访问内部类私有成员(除非声明友元)
-
适用场景
- A类与B类强关联,且A类仅服务于B类时(如链表节点
Node
作为链表List
的内部类) - 通过
private/protected
声明→ 专属内部类(仅限外部类或子类使用)
- A类与B类强关联,且A类仅服务于B类时(如链表节点
-
权限控制
- 内部类放在
public
区 → 允许外部代码创建实例 - 放在
private
区 → 仅限外部类内部使用
- 内部类放在
内部类是C++中嵌套设计的工具,通过封装和权限控制实现专属协作,需权衡封装性与灵活性。
总结
C++类与对象通过封装、构造/析构、拷贝控制等机制,提供了强大的面向对象能力。深入理解this
指针、静态成员、初始化列表等高级特性,能够写出高效且安全的代码。通过对比C与C++的实现,更能体会面向对象的优势。实践中需警惕浅拷贝、内存泄漏等陷阱。