深入解析 C++ 类与对象:从基础语法到高级特性的全面指南

C++ 的类(Class)和对象(Object)是面向对象编程的核心,它们将数据与操作封装为有机整体,实现了对现实世界的抽象建模。本文将从基础定义出发,逐步深入到内存布局、默认成员函数、高级特性等核心内容,结合丰富的代码示例和原理分析,帮助读者全面掌握这一关键知识体系。

一、类的基础定义与核心结构

1. 类的定义语法与访问控制

类是用户自定义类型,通过class关键字定义,包含成员变量(属性)和成员函数(方法),以分号结束:

class Stack {
public:
    // 公共接口:外部可直接调用
    void Init(int n = 4);       // 初始化栈
    void Push(int x);           // 入栈
    int Top();                  // 获取栈顶元素
    void Destroy();             // 销毁栈
private:
    // 私有数据:仅类内可见
    int* array;                 // 存储数据的动态数组
    size_t capacity;            // 栈容量
    size_t top;                 // 栈顶下标(初始为0)
};
  • 访问限定符
    • public:公开成员,类外可直接访问(如st.Push(5))。
    • private:私有成员,仅类内函数或友元可访问(外部访问会编译报错)。
    • protected:保护成员,类内及派生类可访问(继承时使用)。
  • structclass的区别
    struct默认成员访问权限为publicclass默认为private,其他语法完全一致。通常用class定义类,struct定义简单数据结构。

2. 类域与作用域解析符::

类定义了独立的作用域,做声明和定义分离时,类外定义成员函数时需通过::指定所属类域:

// 类外定义Init函数,属于Stack类域
void Stack::Init(int n) {
    array = (int*)malloc(sizeof(int) * n); // 分配内存
    if (!array) perror("malloc failed");
    capacity = n;
    top = 0;
}

若未指定类域,编译器会将Init视为全局函数,导致找不到类成员而报错。

二、对象的实例化与内存布局

1. 实例化过程:从类到对象的具象化

类是模板(不开空间),对象是实例(开空间)。实例化对象时,内存为成员变量分配空间,成员函数存储在公共代码区,不占用对象内存:

Stack st1, st2; // 实例化两个栈对象
  • 成员变量存储:每个对象独立拥有成员变量(如st1.arrayst2.array是不同指针)。
  • 成员函数共享:所有对象共享同一套成员函数代码,通过this指针区分操作的对象(见下文)。

2. 对象大小计算:内存对齐规则

对象大小由成员变量占用的内存决定,需遵循内存对齐规则(提升 CPU 访问效率):

  1. 第一个成员从偏移量 0 开始。
  2. 后续成员对齐到编译器默认对齐数或者成员大小的整数倍位置(VS 默认对齐数为 8,GCC 为 4/8)。
  3. 总大小为最大对齐数的整数倍。
    示例
class A {
    char ch;   // 1字节,对齐到1的倍数(偏移0)
    int i;     // 4字节,对齐到4的倍数(偏移4,补3字节)
};  // 总大小:8字节(1+3+4=8,8是最大对齐数4的倍数)

class B {};  // 空类大小为1字节(占位符,区分对象存在)
  • 指针成员:无论指向什么类型,均占 8 字节(64 位系统)。
  • 忽略成员函数:对象大小仅计算成员变量,成员函数代码不占对象内存。

三、this 指针:对象的隐含管理者

成员函数通过隐含的this指针识别当前操作的对象,由编译器自动传递,无需显式声明:

class Date {
public:
    void SetDate(int year, int month, int day) {
        _year = year;    // 等价于 this->_year = year
        _month = month;  // this指向调用对象(如d1或d2)
        _day = day;
    }
private:
    int _year;
    int _month;
    int  _day;
};
int main()
{
    Date d1, d2;
    d1.SetDate(2024, 10, 1);  // this指向d1
    d2.SetDate(2024, 10, 2);  // this指向d2
    return 0;
}
  • 类型const Date* const this(指向当前对象的常指针,不可修改指向,也不可通过this修改对象成员 )。
  • 应用场景
    • 返回当前对象引用,支持链式调用:
      Date& SetYear(int year)
      {
          _year = year; 
          return *this; // d1.SetYear(2024).SetMonth(10);
      } 
    • 解决成员变量与参数名冲突(如void Init(int capacity) { this->capacity = capacity; })。

四、默认成员函数:编译器的 “自动助手”

当用户未定义时,编译器自动生成 6 个默认成员函数,核心是前 4 个,掌握它们的行为是正确使用类的关键。

1. 构造函数(Constructor):对象的初始化器

  • 作用:在对象实例化时初始化成员变量,替代 C 语言中的Init函数,自动调用。
  • 特性
    • 名称与类名相同,无返回值(连void也不写)。
    • 支持重载:可定义无参、带参、全缺省构造函数,但默认构造函数只能有一个(无参或全缺省)。
    • 默认生成规则:若用户未定义任何构造函数,编译器生成空构造函数(内置类型不初始化,自定义类型调用其默认构造)。若用户定义了任意构造函数,编译器不再生成默认构造函数。
class Stack {
public:
    Stack()
    {
        //...     
    }                // 无参构造
    Stack(int n) 
    {
        //... 
    }          // 带参构造
    Stack(int n = 4) 
    { 
        //... 
    }      // 全缺省构造(与无参构造冲突,不可同时存在)
};
  • 初始化顺序:成员变量按类中声明顺序初始化,与初始化列表顺序无关(见下文 “初始化列表”)。

2. 析构函数(Destructor):对象的资源释放者

  • 作用:对象生命周期结束时自动调用,释放资源(如堆内存、文件句柄等),防止内存泄漏。
  • 特性
    • 名称以~开头,无参数和返回值,一个类只能有一个析构函数。
    • 默认生成规则:用户未定义时,编译器生成空析构函数,但会为自定义类型成员调用其析构函数。
    ~Stack() {
        free(array);       // 释放动态数组
        array = nullptr;   // 置空指针,避免野指针
        capacity = top = 0;
    }
    
  • 调用时机
    • 局部对象离开作用域时(如函数结束)。
    • 动态创建的对象被delete时(如Stack* p = new Stack(); delete p;)。

3. 拷贝构造函数(Copy Constructor):对象的复制器

  • 作用:用已有对象初始化新对象,分为浅拷贝(默认生成,危险)和深拷贝(自定义,安全)。
  • 特性
    • 参数必须为类类型的引用const T&),若用值传递会引发无限递归(因为复制参数时需调用自身拷贝构造)。
    • 默认行为:编译器生成的默认拷贝构造函数对内置类型执行值拷贝(复制指针地址,而非指向的内容),对自定义类型调用其拷贝构造函数。
    • 必写场景:当类包含指针成员(管理动态资源)时,必须自定义拷贝构造函数,否则会导致多个对象指向同一资源,析构时重复释放。
Stack(const Stack& st) //这里必须要用引用,否者会引发无穷递归
{
    // 深拷贝:重新分配内存,复制数据
    capacity = st.capacity;
    top = st.top;
    array = (int*)malloc(capacity * sizeof(int));
    if (array) 
        memcpy(array, st.array, top * sizeof(int));
}

4. 赋值运算符重载(operator=):对象的赋值器

  • 作用:实现两个已有对象的赋值操作,需处理自赋值和资源释放。
  • 特性
    • 成员函数形式,返回T&以支持链式赋值(如a = b = c;)。
    • 默认行为:与默认拷贝构造类似,对内置类型值拷贝,自定义类型调用其operator=
    Stack& operator=(const Stack& st) {
        if (this != &st) { // 避免自赋值(释放自身资源前检查)
            free(array);   // 释放旧资源
            // 深拷贝逻辑(同拷贝构造)
            capacity = st.capacity;
            top = st.top;
            array = (int*)malloc(capacity * sizeof(int));
            if (array) memcpy(array, st.array, top * sizeof(int));
        }
        return *this;
    }
    

五、初始化列表:更高效的成员初始化方式

在构造函数中,使用成员列表进行初始化,比函数体内赋值更高效,尤其适用于特殊成员:

class Data {
public:
    // 初始化列表:直接初始化成员
    Data(int x, int y) 
        : _x(x)
        , _y(y) 
    {
        
    } 
    // 等价于:Data(int x, int y) { _x = x; _y = y; }(但赋值方式效率更低)
private:
    int _x;
    int _y;
};
  • 必须使用初始化列表的场景(也可以在定义处给缺省值)
    1. 引用成员:引用必须在定义时初始化,无法在函数体内赋值。
    2. const成员:常量必须在初始化时赋值,无法后续修改。
    3. 无默认构造函数的自定义类型成员:若成员类型没有默认构造函数,必须在初始化列表显式调用其带参构造函数。
class RefData {
public:
    RefData(int& ref)
    : _ref(ref)
    {
    
    } // 正确,初始化列表初始化引用
    // RefData(int& ref) { _ref = ref; } 错误,引用未初始化
private:
    int& _ref;
};
  • 初始化顺序:成员按类中声明顺序初始化,与初始化列表顺序无关。错误顺序可能导致未定义行为:
    class A {
    public:
        A(int x) 
        : b(a)
        , a(x) 
        {
    
        } //成员a先声明,初始化顺序应为a→b,a和b的值应相等
    private:
        int a;
        int b;
    }; 
    

成员变量走初始化列表的逻辑

六、静态成员:类级别的共享资源

static修饰的成员属于类本身,而非某个对象,适用于全局共享的数据或工具函数。

1. 静态成员变量

  • 特性
    • 存储在静态区,所有对象共享,通过类名::变量对像.变量访问。
    • 必须在类外初始化(声明时不可赋值,否则重复定义):
      class A {
      public:
          int GetCount()
          {
              return count;
          }
      
      private:
          static int count; // 类内声明
      };
      int A::count = 0; // 类外初始化,且需指定类域
      int main()
      {
          A a1;
          cout<<a1.GetCount()<<endl;
          cout<<A::GetCount()<<endl;
          return 0;
      }
  • 应用场景:统计类的实例数量、全局配置参数等。

2. 静态成员函数

  • 特性
    • this指针,只能访问静态成员(静态变量 / 静态函数),不能访问非静态成员。
    class Math {
    public:
        static int Add(int a, int b) 
        {
            return a + b; 
        } // 静态函数,无this指针
        int NonStaticAdd(int a, int b) 
        { 
            return a + b + x; 
        } // 非静态函数,可访问非静态成员x
    private:
        static int x; // 静态变量
        int y; // 非静态变量
    };
    
  • 调用方式:直接通过类名调用,无需实例化对象:
    int sum = Math::Add(3, 5); // 直接调用静态函数
    

七、友元:突破封装的双刃剑

允许非类成员访问类的私有 / 保护成员,分为友元函数友元类,但会破坏封装性,需谨慎使用。

1. 友元函数

  • 声明:在类内用friend关键字声明外部函数,使其获得访问权限:
    class Date {
    friend ostream& operator<<(ostream& out, const Date& d); // 友元函数声明
    private:
        int _year, _month, _day;
    };
    // 外部定义友元函数,可访问私有成员
    ostream& operator<<(ostream& out, const Date& d) {
        out << d._year << "-" << d._month << "-" << d._day;
        return out;
    }
    
  • 特点:友元函数不是类的成员函数,不受访问限定符位置影响(可在类内任意位置声明)。

2. 友元类

  • 声明:类 A 声明类 B 为友元,则 B 的所有成员函数均可访问 A 的私有 / 保护成员:
    class A {
    friend class B; // B是A的友元类
    private:
        int secret;
    };
    class B {
    public:
        void AccessSecret(A& a) 
        { 
            a.secret = 100; // 合法访问A的私有成员
        } 
    };
    
  • 单向性:友元关系不具有交换性(A 是 B 的友元,B 不一定是 A 的友元)。

八、内部类:嵌套的类结构

定义在另一个类内部的类,作为独立类存在,默认是外部类的友元(可访问外部类私有成员)。

1. 分类与特性

  • 静态内部类:用static修饰,不依赖外部类实例,只能访问外部类的静态成员。
  • 非静态内部类:依赖外部类实例,可访问外部类的所有成员(包括私有成员)。
class Outer {
public:
    class Inner { // 非静态内部类,默认是Outer的友元
    public:
        void Print(Outer& o) 
        { 
            cout << o.privateData; 
        } // 访问外部类私有成员
    };
private:
    int privateData;
};


2. 应用场景

封装与外部类紧密相关的辅助类,如STL容器的迭代器常作为内部类实现。

九、匿名对象与编译器优化

1. 匿名对象:临时存在的 “一次性” 对象

无名称的临时对象,用类名创建,生命周期仅当前语句,语句结束后立即析构:

class Printer {
public:
    Printer(string msg) {
        cout << msg << endl; 
    }
    ~Printer() { 
        cout << "Destroy Printer" << endl; 
    }
};
int main()
{
    // 匿名对象示例
    Printer("Hello, World!"); // 输出信息后立即销毁,打印"Destroy Printer"
    return 0;
}
  • 应用场景:临时调用某个类的方法,无需保留对象(如Stack().Push(1);入栈后销毁)。

2. 拷贝构造优化:编译器的 “偷工减料”

现代编译器会优化不必要的拷贝构造,提升性能,常见优化包括:

  • RVO(返回值优化):当函数返回本地对象时,直接构造调用处的对象,避免临时对象创建。
    A f() { 
        return A(1); // 优化后,直接在调用处构造A(1),不调用拷贝构造。
    } 
    
  • NRVO(命名返回值优化):对命名的本地对象(如A aa; return aa;)进行类似优化。
  • 关闭优化:调试时可通过编译器选项(如g++ -fno-elide-constructors)关闭优化,观察真实拷贝行为。

十、类设计最佳实践与常见陷阱

1. 资源管理三原则

  • Rule of Three:若类需要自定义析构函数(释放资源),则必须同时自定义拷贝构造函数和赋值运算符,避免浅拷贝导致的资源错误。
  • Rule of Five(C++11 后):新增移动构造和移动赋值,处理右值引用时的高效资源转移。

2. 避免默认构造函数冲突

确保类有且仅有一个默认构造函数(无参或全缺省),否则会导致实例化时的歧义:

class BadClass {
    BadClass() {}          // 无参构造
    BadClass(int = 0) {}  // 全缺省构造(与无参构造冲突,编译报错)
};

3. const成员函数:保护对象的 “只读” 接口

const修饰成员函数,确保函数内不修改对象成员,提高代码安全性:

class ConstDemo {
public:
    void Print() const { // this指针为const ConstDemo* const
        // _x = 10; 错误,不可修改成员
        cout << _x << endl;
    }
private:
    int _x;
};
const ConstDemo obj;
obj.Print(); // 合法,调用const成员函数

总结:类与对象的核心价值

C++ 的类与对象通过封装(数据隐藏)、初始化与资源管理(构造 / 析构)、拷贝控制(深拷贝)等机制,实现了对复杂数据结构的安全高效管理。理解this指针、默认成员函数、初始化列表等底层机制,是写出健壮代码的关键。同时,合理使用静态成员、友元、内部类等高级特性,能进一步提升代码的抽象能力和复用性。

在实际开发中,始终遵循 “封装优先” 原则,谨慎处理资源管理,避免浅拷贝陷阱,结合编译器优化与最佳实践,才能充分发挥 C++ 面向对象编程的强大能力。类与对象不仅是语法知识,更是一种建模思维,掌握它们是深入 C++ 世界的必经之路。

练习代码

//可根据该文件中的内容完善Date.cpp
//Date.h
#include<iostream>
#include<assert.h>
using namespace std;
class Date

{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1) 
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	// 获取某年某月的天数
	int GetMonthDay(int year, int month) {
		assert(month > 0 && month < 13);
		int arr[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;
		}
		return arr[month];
	}

	//检查日期合法性
	bool CheckDate();


	// >运算符重载
	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;
	
	//赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d);


	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);
	// 日期-天数
	Date operator-(int day);
	// 日期-=天数
	Date& operator-=(int day);



	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);
	// 前置--
	Date& operator--();
	// 后置--
	Date operator--(int);
	// 日期-日期 返回天数
	int operator-(const Date& d);

private:

	int _year;

	int _month;

	int _day;

};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
//Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
ostream& operator<<(ostream& out, const Date& d)
{
    cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    return out;
}
istream& operator>>(istream& in, Date& d)
{
    while (1)
    {
        cout << "请输入日期:";
        cin >> d._year >> d._month >> d._day;
        if (!d.CheckDate())
        {
            cout << "日期非法,请重新输入" << endl;
        }
        else
            break;
    }
    return in;
}
bool Date::CheckDate()
{
    if (_month < 1 || _month > 12  || _day < 1 || _day > GetMonthDay(_year, _month))
    {
        return false;
    }
    else
    {
        return true;
    }
}


bool Date::operator>(const Date& d) const {
    if (_year > d._year) {
        return true;
    }
    else if (_year == d._year) {
        if (_month > d._month)
            return true;
        else if (_month == d._month)
            return _day - d._day;

    }
    return false;
}
// ==运算符重载
bool Date::operator==(const Date& d) const {
    //if (_year == d._year && _month == d._month && _day == d._day)
    //    return true;
    //else
    //    return false;
    return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool Date::operator >= (const Date& d)const {
    //if ((*this) > d || (*this) == d)
    //    return true;
    //else
    //    return false;
    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=(const Date& d) {
	if (*this == d) {
		return *this;
	}
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}


// 日期+=天数
Date& Date::operator+=(int day) {
    _day += day;
    while (_day >= GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        _month++;
        if (_month == 13)
        {
            _month = 1;
            _year++;
        }
    }
    return *this;
}
// 日期+天数
Date Date::operator+(int day) {
    Date tmp(*this);
    tmp += day;
    return tmp;
}
// 日期-天数
Date Date::operator-(int day) {
    Date tmp(*this);
    tmp -= day;
    return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day) {
    _day -= day;
    while (_day <= 0)
    {
        _month--;
        if (_month == 0)
        {
            _month = 12;
            _year--;
        }
        _day += GetMonthDay(_year, _month);
    }
    return *this;
}

// 前置++
Date& Date::operator++() {
    *this += 1;
    return *this;
}
// 后置++
Date Date::operator++(int) {
    Date tmp(*this);
    ++(*this);
    return tmp;
}
// 前置--
Date& Date::operator--() {
    *this -= 1;
    return *this;
}
// 后置--
Date Date::operator--(int) {
    Date tmp(*this);
    --(*this);
    return tmp;
}
// 日期-日期 返回天数
int Date::operator-(const Date& d) {
    Date max = *this;
    Date min = d;
    int flag = 1;
    if (max < d)
    {
        max = d;
        min = *this;
        flag = -1;
    }
    int count = 0;
    while (max != min)
    {
        --max;
        count++;
    }
    return flag*count;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值