C++基础:类和对象

文章目录

  • 类和对象一
  • 类和对象二
    • [1. 类的6个默认成员函数](#1. 类的6个默认成员函数)
      • [1.1 什么是默认成员函数](#1.1 什么是默认成员函数)
      • [1.2 学习两个核心问题](#1.2 学习两个核心问题)
      • [1.3 两种数据类型区分](#1.3 两种数据类型区分)
    • [2. 构造函数(对象出生时自动初始化)](#2. 构造函数(对象出生时自动初始化))
      • [2.1 作用](#2.1 作用)
      • [2.2 7条特点(大白话)](#2.2 7条特点(大白话))
      • [2.3 简单示例](#2.3 简单示例)
      • [2.4 易错点](#2.4 易错点)
    • [3. 析构函数(对象死亡时清理资源)](#3. 析构函数(对象死亡时清理资源))
      • [3.1 作用](#3.1 作用)
      • [3.2 8条特点](#3.2 8条特点)
      • [3.3 简单示例](#3.3 简单示例)
    • [4. 拷贝构造函数(用老对象造新对象)](#4. 拷贝构造函数(用老对象造新对象))
      • [4.1 作用](#4.1 作用)
      • [4.2 6条特点](#4.2 6条特点)
      • [4.3 基础示例](#4.3 基础示例)
    • [5. 赋值运算符重载(两个现成对象互相复制)](#5. 赋值运算符重载(两个现成对象互相复制))
      • [5.1 运算符重载基础](#5.1 运算符重载基础)
        • [5.1.1 概念](#5.1.1 概念)
        • [5.1.2 基础规则](#5.1.2 基础规则)
      • [5.2 赋值重载 operator=](#5.2 赋值重载 operator=)
        • [5.2.1 和拷贝构造核心区别](#5.2.1 和拷贝构造核心区别)
        • [5.2.2 特点](#5.2.2 特点)
        • [5.2.3 简单示例](#5.2.3 简单示例)
    • [6. const成员函数 + 取地址重载](#6. const成员函数 + 取地址重载)
      • [6.1 const成员函数](#6.1 const成员函数)
        • [6.1.1 写法](#6.1.1 写法)
        • [6.1.2 作用](#6.1.2 作用)
        • [6.1.3 调用规则](#6.1.3 调用规则)
        • [6.1.4 示例](#6.1.4 示例)
      • [6.2 取地址运算符重载](#6.2 取地址运算符重载)
        • [6.2.1 两种重载](#6.2.1 两种重载)
        • [6.2.2 使用场景](#6.2.2 使用场景)
        • [6.2.3 极简示例](#6.2.3 极简示例)
    • [7. const写在函数前、后,以及前后都写的区别](#7. const写在函数前、后,以及前后都写的区别)
      • [7.1 const 在函数后方(成员函数专属)](#7.1 const 在函数后方(成员函数专属))
      • [7.2 const 在函数前方(修饰返回值)](#7.2 const 在函数前方(修饰返回值))
      • [7.3 const 前后都写](#7.3 const 前后都写)
  • 类和对象三
    • [1. 构造函数初始化列表](#1. 构造函数初始化列表)
      • [1.1 基础语法](#1.1 基础语法)
      • [1.2 三大**必须**放初始化列表的成员(函数体内赋值报错)](#1.2 三大必须放初始化列表的成员(函数体内赋值报错))
      • [1.3 核心规则](#1.3 核心规则)
      • [1.4 易错示例](#1.4 易错示例)
    • [2. 隐式类型转换 & explicit关键字](#2. 隐式类型转换 & explicit关键字)
      • [2.1 隐式转换规则](#2.1 隐式转换规则)
      • [2.2 explicit作用](#2.2 explicit作用)
    • [3. static 静态成员](#3. static 静态成员)
      • [3.1 静态成员变量](#3.1 静态成员变量)
      • [3.2 静态成员函数](#3.2 静态成员函数)
      • [3.3 访问权限](#3.3 访问权限)
      • [3.4 经典示例:统计对象个数](#3.4 经典示例:统计对象个数)
      • [3.5 经典考题:1+2+...+n](#3.5 经典考题:1+2+…+n)
    • [4. 友元 friend](#4. 友元 friend)
      • [4.1 友元函数](#4.1 友元函数)
      • [4.2 友元类](#4.2 友元类)
      • [4.3 两大特性](#4.3 两大特性)
      • [4.4 优缺点](#4.4 优缺点)
    • [5 内部类(嵌套类)](#5 内部类(嵌套类))
      • [5.1 定义](#5.1 定义)
      • [5.2 核心特点](#5.2 核心特点)
      • [5.3 使用场景](#5.3 使用场景)
    • [6 匿名对象](#6 匿名对象)
      • [6.1 定义](#6.1 定义)
      • [6.2 区分易错](#6.2 区分易错)
    • [7 对象拷贝的编译器优化](#7 对象拷贝的编译器优化)
      • [7.1 优化目的](#7.1 优化目的)
      • [7.2 典型优化场景](#7.2 典型优化场景)
      • [7.3 不优化场景](#7.3 不优化场景)
      • [7.4 关闭优化](#7.4 关闭优化)
      • [7.5 优化差异](#7.5 优化差异)

类和对象一

1. 类的定义

1.1 类定义格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

类体中内容称为类的成员

  • 类中的变量:类的属性 / 成员变量
  • 类中的函数:类的方法 / 成员函数

规范习惯:为区分成员变量,常在变量前后加_、或用m开头(C++非强制,按公司编码规范)。

  1. C++中struct也可以定义类
    • C++兼容C语言struct用法,同时struct升级为类,支持内部定义函数;
    • 日常开发优先用class定义类;
    • 定义在类内的成员函数默认是inline内联函数。
示例1:Stack类基础定义
cpp 复制代码
#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;
}
示例2:Date类(成员变量带下划线区分)
cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    // 习惯:成员变量加下划线区分局部变量
    int _year; // year_ / m_year 也可
    int _month;
    int _day;
};

int main()
{
    Date d;
    d.Init(2024, 3, 31);
    return 0;
}
示例3:C++ struct 升级对比
cpp 复制代码
#include<iostream>
using namespace std;

// C语言风格struct(需要typedef)
typedef struct ListNodeC
{
    struct ListNodeC* next;
    int val;
}LTNode;

// C++ struct:无需typedef,自带类型名,可包含成员函数
struct ListNodeCPP
{
    void Init(int x)
    {
        next = nullptr;
        val = x;
    }
    ListNodeCPP* next;
    int val;
};

int main()
{
    return 0;
}

1.2 访问限定符

访问限定符是C++实现封装的核心方式:把属性、方法封装在类内,通过权限控制对外暴露接口。

  • public:公有成员,类外可直接访问
  • private:私有成员,类外不可直接访问(默认权限)
  • protected:保护成员,类外不可直接访问;仅在继承时和private有区别

规则:

  1. 权限作用域:从当前限定符开始,到下一个限定符为止;无后续限定符则到类末尾}
  2. class默认权限:privatestruct默认权限:public
  3. 工程规范:成员变量设为private/protected,对外提供调用的成员函数设为public

1.3 类域

类定义了独立的作用域,所有类成员都属于该类域。

  • 若成员函数声明与定义分离 ,类外实现时必须用类名::作用域操作符指明所属类;
  • 编译器查找规则:先在局部域查找,找不到则去类域查找成员变量。
示例:声明定义分离 + 类域::
cpp 复制代码
#include<iostream>
using namespace std;

class Stack
{
public:
    // 函数声明
    void Init(int n = 4);
private:
    int* array;
    size_t capacity;
    size_t top;
};

// 类外定义函数,必须加 Stack:: 指定类域
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;
}

2. 实例化

2.1 实例化概念

  1. 实例化:使用类类型在内存中创建对象的过程;
  2. 类是抽象模板,仅声明成员变量,不分配内存;实例化对象时才会分配物理内存存储数据;
  3. 一个类可以实例化多个独立对象,每个对象拥有独立成员变量空间;
  4. 通俗比喻:类 = 建筑图纸,对象 = 按图纸造出的实体房子;图纸不能住,房子才能存数据。
示例:Date实例化多个对象
cpp 复制代码
#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()
{
    // 实例化两个独立对象d1、d2
    Date d1;
    Date d2;

    d1.Init(2024, 3, 31);
    d1.Print();

    d2.Init(2024, 7, 5);
    d2.Print();
    return 0;
}

2.2 对象大小

存储核心结论
  1. 对象仅存储成员变量,不存储成员函数;
  2. 成员函数编译为指令,统一存放在公共代码段,所有对象共享同一份函数代码;
  3. 若每个对象都存函数指针会造成大量内存浪费;普通场景编译期直接绑定函数地址,无需存储指针;多态虚函数场景才会保存虚表指针。

两种存储设计对比:

  • 设计一(废弃):对象内部同时存成员变量+所有成员函数,极度浪费空间;
  • 设计二(C++标准):每个对象独占成员变量;成员函数全局共享公共代码区。
内存对齐规则(类对象遵循和结构体相同规则)

使用原因:CPU 读取内存不是一字节一字节读,而是按固定块(4/8 字节)批量读取。

  • 若数据地址不对齐,CPU 需要读两次内存再拼接,效率极低;
  • 部分硬件架构(ARM)不支持非对齐访问,直接崩溃;
  • 编译器自动填充空白字节(填充 padding),保证每个成员地址对齐

对齐数 = min (编译器默认对齐数,当前成员变量自身大小)

  1. 第一个成员变量偏移地址为0;
  2. 其他成员对齐到「对齐数」整数倍地址:对齐数 = 编译器默认对齐数变量自身大小 的较小值;
    • VS默认对齐数:8;
  3. 类/结构体总大小 = 最大对齐数的整数倍;
  4. 嵌套结构体:嵌套子结构体对齐到自身最大对齐数倍数,整体大小取全部最大对齐数公倍数。
空类/无成员变量类大小特殊规则
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
private:
    char _ch;
    int _i;
};

class B
{
public:
    void Print(){}
};

class C
{};

int main()
{
    A a;
    B b;
    C c;
    cout << sizeof(a) << endl;  // 内存对齐计算
    cout << sizeof(b) << endl;  // 1字节
    cout << sizeof(c) << endl;  // 1字节
    return 0;
}

空类、只有成员函数无成员变量的类,实例化对象占1字节:仅用于占位,标识该对象真实存在,区分不同空对象地址。

3. this指针

核心问题

同一个类多个对象调用同名成员函数,函数如何区分当前操作的是哪个对象?

this指针原理

  1. 编译器编译成员函数时,会自动在形参第一个位置 添加当前类指针参数Date* const this
    例如void Init(int year,int month,int day)真实原型:

    cpp 复制代码
    void Init(Date* const this, int year, int month, int day)
  2. 对象调用成员函数时,编译器自动把对象地址传给this形参;

  3. 函数内访问成员变量本质是this->_yearthis->可省略;

  4. 语法限制:不能手动在形参/实参位置写this;函数体内可显式使用this

  5. thisDate* const类型:指针本身不可修改(不能this = nullptr),指向的内容可修改。

this指针完整示例
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
    // 隐藏参数 Date* const this
    void Init(int year, int month, int day)
    {
        // this = nullptr;  // 编译报错,this指针不可修改指向
        _year = year;          // 等价 this->_year = year;
        this->_month = month;
        this->_day = day;
    }
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    Date d2;

    // 编译器底层:d1.Init(&d1, 2024, 3, 31);
    d1.Init(2024, 3, 31);
    d1.Print();

    d2.Init(2024, 7, 5);
    d2.Print();
    return 0;
}

配套选择题

第1题
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、编译报错 B、运行崩溃 C、正常运行

答案选C.

解析:首先,我们知道成员函数不存放在对象里,全部存放在公共代码段,当p->Print()调用函数时,不会去p的对象里寻找,而是直接锁定 Print 函数固定代码地址,直接调用。而Print()函数内部也没有调用对象里的内容,所以p并没有被解引用,也就不会访问空指针,程序就不会崩溃.

选学拓展:编译器如何在公共代码段找到 Print 函数

类的普通成员函数统一存放在程序公共代码段,编译、链接阶段编译器就会完成函数地址绑定,运行时完全不需要依靠对象指针查找函数。编译器识别p->Print()时,只会根据指针p的类型A*匹配对应类的A::Print,并从符号表中读取该函数在代码段的固定内存地址,将代码直接转换为固定跳转指令,仅把p的值作为隐藏形参this传入函数;整个查找过程与p是否为空、p指向的对象内存无关。只有带virtual修饰的虚函数才需要运行时通过对象内部虚表指针查找函数地址,本题 Print 为普通成员函数,不存在该步骤,因此哪怕p = nullptr,程序也能正常定位并进入 Print 函数执行,再加上函数内部没有访问对象成员、不会对空指针this解引用,最终程序正常运行。

第2题
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;
}

选项:A、编译报错 B、运行崩溃 C、正常运行

答案:B;

解析:这里的_a实际为this->_a,空指针遭到解引用,程序崩溃;

4. C vs C++ Stack 对比(封装演示)

面向对象三大特性:封装、继承、多态,Stack对比直观体现封装思想。

核心变化

  1. C语言:数据(结构体)、操作函数完全分离,可随意修改结构体成员;
  2. C++:数据+成员函数封装进类,通过private限制外部直接访问,仅暴露public接口,规范数据读写;
  3. C++语法便利:缺省参数、无需手动传对象地址(this指针隐式传递)、不用typedef简化类型使用。

C语言实现栈

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int STDataType;
typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
}ST;

void STInit(ST* ps)
{
    assert(ps);
    ps->a = NULL;
    ps->top = 0;
    ps->capacity = 0;
}

void STDestroy(ST* ps)
{
    assert(ps);
    free(ps->a);
    ps->a = NULL;
    ps->top = ps->capacity = 0;
}

void STPush(ST* ps, STDataType x)
{
    assert(ps);
    if (ps->top == ps->capacity)
    {
        int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
        if (tmp == NULL)
        {
            perror("realloc fail");
            return;
        }
        ps->a = tmp;
        ps->capacity = newcapacity;
    }
    ps->a[ps->top] = x;
    ps->top++;
}

bool STEmpty(ST* ps)
{
    assert(ps);
    return ps->top == 0;
}

void STPop(ST* ps)
{
    assert(ps);
    assert(!STEmpty(ps));
    ps->top--;
}

STDataType STTop(ST* ps)
{
    assert(ps);
    assert(!STEmpty(ps));
    return ps->a[ps->top - 1];
}

int STSize(ST* ps)
{
    assert(ps);
    return ps->top;
}

int main()
{
    ST s;
    STInit(&s);
    STPush(&s, 1);
    STPush(&s, 2);
    STPush(&s, 3);
    STPush(&s, 4);
    while (!STEmpty(&s))
    {
        printf("%d\n", STTop(&s));
        STPop(&s);
    }
    STDestroy(&s);
    return 0;
}

C++实现栈

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
    void Init(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }

    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _a = tmp;
            _capacity = newcapacity;
        }
        _a[_top++] = x;
    }

    void Pop()
    {
        assert(_top > 0);
        --_top;
    }

    bool Empty()
    {
        return _top == 0;
    }

    int Top()
    {
        assert(_top > 0);
        return _a[_top - 1];
    }

    void Destroy()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    // 私有成员变量,外部无法直接访问
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);
    while (!s.Empty())
    {
        printf("%d\n", s.Top());
        s.Pop();
    }
    s.Destroy();
    return 0;
}

类和对象二

1. 类的6个默认成员函数

1.1 什么是默认成员函数

你写一个类,不写任何函数时,编译器偷偷自动生成的函数,就叫默认成员函数。

一共6个:构造、析构、拷贝构造、赋值重载、普通取地址、const取地址。

1.2 学习两个核心问题

  1. 编译器自动生成的函数能干啥?够不够用?
  2. 不够用时,自己该怎么写?

1.3 两种数据类型区分

  1. 内置类型:int、char、double、指针(C++自带基础类型)
  2. 自定义类型:自己用class写的类(Date、Stack)

2. 构造函数(对象出生时自动初始化)

2.1 作用

对象刚创建时,自动给成员变量赋初始值,不用手动调用Init初始化函数。

注意:不是构造函数开辟内存,局部对象的内存本来就开好,构造只负责赋值。

2.2 7条特点(大白话)

  1. 函数名字必须和类名一模一样
  2. 没有返回值,不能写void
  3. 创建对象时,系统自动调用,不用手动调用
  4. 可以写多个构造(函数重载)
  5. 只要你手动写了任意一个构造,编译器就不再自动生成无参构造
  6. 默认构造:不用传参数就能调用的构造,分三种:
    • 编译器自动生成的无参构造
    • 自己写的无参构造 Date()
    • 全缺省构造 Date(int y=1,int m=1,int d=1)
      无参和全缺省不能同时写,创建对象会分不清调用谁,报错。
  7. 自动构造初始化规则:
    • 内置类型:值随机,不会清零
    • 自定义类型成员:自动调用它自己的无参构造;如果这个类没有无参构造,代码报错

2.3 简单示例

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    // 无参构造(默认构造)
    Date()
    {
        _y = 2000;
    }
    // 带参构造
    Date(int y)
    {
        _y = y;
    }
    void Show()
    {
        cout << _y << endl;
    }
private:
    int _y;
};
int main()
{
    Date d1;      // 调用无参构造
    Date d2(2025);// 调用带参构造
    d1.Show();
    d2.Show();
    return 0;
}

2.4 易错点

写无参对象不能加括号 Date d3();,编译器会当成函数声明,不是创建对象。

3. 析构函数(对象死亡时清理资源)

3.1 作用

对象销毁前自动调用,释放堆上手动开辟的内存(malloc/new出来的空间)。

对象本身占用的栈内存系统自动回收,析构只管堆资源清理,替代C语言Destroy函数。

3.2 8条特点

  1. 名字:~加类名 ~Date()
  2. 无参数、无返回值
  3. 一个类只能有1个析构,不能重载
  4. 对象生命周期结束,系统自动调用
  5. 默认析构行为:
    • 内置类型:啥也不干
    • 自定义类型成员:自动调用它的析构
  6. 不管写不写析构,里面的自定义成员都会自动调用析构
  7. 要不要手动写析构:
    • 没有堆内存(Date只有int):不用写,编译器默认够用
    • 有堆内存(Stack里存指针数组):必须手写,否则内存泄漏
  8. 局部对象销毁顺序:先创建的后销毁,后创建的先销毁

3.3 简单示例

cpp 复制代码
#include<iostream>
#include<cstdlib>
using namespace std;
class Stack
{
public:
    Stack()
    {
        arr = (int*)malloc(sizeof(int)*10);
    }
    // 析构释放堆内存
    ~Stack()
    {
        cout << "释放堆空间" << endl;
        free(arr);
        arr = nullptr;
    }
private:
    int* arr;
};
int main()
{
    Stack st;
    // 函数结束,st销毁,自动执行析构
    return 0;
}

4. 拷贝构造函数(用老对象造新对象)

4.1 作用

拿一个已经存在的同类对象,复制一份生成全新对象。

标准写法:Date(const Date& d)

4.2 6条特点

  1. 属于构造函数的重载
  2. 第一个参数必须是同类引用,不能直接传对象(会无限递归,直接报错)
  3. 三种场景会自动调用拷贝构造:
    1. 对象作为函数参数,值传递
    2. 函数返回对象,值返回
    3. Date d2(d1); Date d2 = d1; 用旧对象创建新对象
  4. 不手写,编译器自动生成默认拷贝构造:
    • 内置变量:浅拷贝(直接复制数字、地址)
    • 自定义成员:调用它自己的拷贝构造
  5. 什么时候需要手动写:
    • 无堆内存:默认拷贝够用,不用写
    • 有堆内存:默认浅拷贝会让两个对象共用一块堆内存,销毁时释放两次程序崩溃,必须手写深拷贝(重新开一块独立内存)
  6. 返回值区别:
    • 值返回:生成临时对象,调用拷贝构造,效率低
    • 引用返回:不拷贝;但不能返回局部对象的引用(局部对象销毁,引用变成野引用)

4.3 基础示例

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    Date(int y=2000)
    {
        _y = y;
    }
    // 拷贝构造
    Date(const Date& d)
    {
        cout << "执行拷贝构造" << endl;
        _y = d._y;
    }
    void Show()
    {
        cout << _y << endl;
    }
private:
    int _y;
};
int main()
{
    Date d1(2025);
    Date d2(d1);  // 拷贝构造
    Date d3 = d1; // 拷贝构造
    d2.Show();
    d3.Show();
    return 0;
}

5. 赋值运算符重载(两个现成对象互相复制)

5.1 运算符重载基础

5.1.1 概念

让±*/==++这些运算符支持类对象运算,本质是特殊函数,名字统一为 operator运算符

5.1.2 基础规则
  1. 二元运算符写成成员函数,只传1个参数(左边是this对象)
  2. 运算符优先级、结合顺序不会改变
  3. 5个运算符不能重载:.* :: sizeof ?: .
  4. 重载必须带至少一个自定义类型,不能修改int等内置类型运算
  5. 区分前置++、后置++:后置多加一个没用的int参数区分重载
  6. 流运算符<<、>>必须写成全局函数,配合友元访问私有变量

5.2 赋值重载 operator=

5.2.1 和拷贝构造核心区别
  1. 拷贝构造:创建新对象,用旧对象初始化
  2. 赋值重载:两个已经存在的对象,互相赋值覆盖内容
5.2.2 特点
  1. 只能写成类内部成员函数,不能全局
  2. 参数推荐const引用,减少拷贝开销
  3. 返回值推荐类引用,支持连续赋值 d1=d2=d3,返回*this
  4. 不手写编译器生成默认赋值,和默认拷贝构造一样是浅拷贝
  5. 手动写了析构释放堆内存,就必须手动写赋值重载实现深拷贝
5.2.3 简单示例
cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    Date(int y=2000)
    {
        _y = y;
    }
    // 赋值重载
    Date& operator=(const Date& d)
    {
        // 防止自己赋值自己
        if(this != &d)
        {
            _y = d._y;
        }
        return *this;
    }
    void Show()
    {
        cout << _y << endl;
    }
private:
    int _y;
};
int main()
{
    Date d1(2025);
    Date d2(1999);
    d2 = d1; // 调用赋值重载,两个对象都已经存在
    d2.Show();
    return 0;
}

6. const成员函数 + 取地址重载

6.1 const成员函数

6.1.1 写法

函数括号后面加const:void Show() const

6.1.2 作用

修饰隐含的this指针,函数内部不允许修改任何成员变量,只做读取操作。

6.1.3 调用规则
  1. 普通对象:能调用普通函数、const函数
  2. const常量对象:只能调用const成员函数,不能调用普通成员函数
6.1.4 示例
cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    Date(int y=2000)
    {
        _y = y;
    }
    // const只读函数
    void Show() const
    {
        cout << _y << endl;
    }
private:
    int _y;
};
int main()
{
    Date d1;
    const Date d2; // 常量对象
    d1.Show();
    d2.Show();     // 常量对象只能调用const函数
    return 0;
}

6.2 取地址运算符重载

6.2.1 两种重载
  1. 普通对象取地址:Date* operator&()
  2. const对象取地址:const Date* operator&() const
6.2.2 使用场景

绝大多数情况不用自己写,编译器默认实现够用;只有特殊需求(隐藏对象真实地址)才手动改写。

6.2.3 极简示例
cpp 复制代码
class Date
{
public:
    Date* operator&()
    {
        return this;
    }
    const Date* operator&() const
    {
        return this;
    }
private:
    int _y;
};

7. const写在函数前、后,以及前后都写的区别

7.1 const 在函数后方(成员函数专属)

void Print() const

修饰this指针,函数内部不能修改成员;const对象仅能调用这类函数。

7.2 const 在函数前方(修饰返回值)

const Date Func()

限制函数返回的临时对象不可修改;返回内置类型无意义。

7.3 const 前后都写

const Date Func() const

两层限制同时生效:

  1. 末尾const:函数内不能修改当前对象成员;
  2. 开头const:函数返回的对象不能被修改。

类和对象三

1. 构造函数初始化列表

1.1 基础语法

构造函数后加 :,逗号分隔成员 成员(初始值)

cpp 复制代码
Date(int y)
:_year(y), _month(1)
{}

1.2 三大必须放初始化列表的成员(函数体内赋值报错)

  1. 引用成员 int& _ref
  2. const常量成员 const int _n
  3. 无默认构造的自定义类型成员 Time _t

1.3 核心规则

  1. 每个成员在列表只能初始化1次;
  2. 初始化顺序 = 类内声明顺序,和列表书写顺序无关(极易踩坑);
  3. 无论写不写初始化列表,所有成员都会先走初始化列表,再进函数体;
  4. 类内声明处写 int _year = 1 是缺省值,仅给未在列表显式初始化的成员使用;
  5. 优先使用初始化列表,避免先默认构造再赋值,提升效率。

1.4 易错示例

cpp 复制代码
class A
{
public:
    A(int a)
    :_a1(a), _a2(_a1)
    {}
private:
    int _a2; // 先初始化(声明在前)
    int _a1; // 后初始化
};
// 输出:1 随机值,_a2初始化时_a1还未赋值

2. 隐式类型转换 & explicit关键字

2.1 隐式转换规则

单参/多参带缺省值的构造函数,支持内置类型自动转类对象:

cpp 复制代码
class A{ A(int x){} };
A a = 1; // 隐式转换,int临时对象构造再拷贝,编译器优化为直接构造
A b = {2,3}; // C++11多参数隐式转换

2.2 explicit作用

修饰构造函数,禁止隐式转换,只能显式调用:

cpp 复制代码
explicit A(int x){}
A a = 1; // 编译报错
A a(1);  // 合法显式构造

3. static 静态成员

3.1 静态成员变量

  1. 存储在静态区,不属于单个对象,所有对象共享;
  2. 类内仅声明,必须类外初始化;不能在类内给缺省值;
cpp 复制代码
class A{ static int _count; };
int A::_count = 0; // 类外初始化

3.2 静态成员函数

  1. 无隐含 this 指针;
  2. 仅能访问静态成员,不能访问普通成员;
  3. 调用方式:类名::静态函数() / 对象.静态函数()

3.3 访问权限

静态成员同样受 public/protected/private 控制,私有静态外部无法直接访问。

3.4 经典示例:统计对象个数

cpp 复制代码
class A
{
public:
    A(){ ++_cnt; }
    A(const A&){ ++_cnt; }
    ~A(){ --_cnt; }
    static int GetCount(){ return _cnt; }
private:
    static int _cnt;
};
int A::_cnt = 0;

3.5 经典考题:1+2+...+n

利用变长数组创建n个对象,构造函数累加静态变量:

cpp 复制代码
class Sum
{
public:
    Sum(){ _ret += _i++; }
    static int GetRes(){ return _ret; }
private:
    static int _i, _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;

int Sum_Solution(int n)
{
    Sum arr[n]; // 创建n个Sum对象,执行n次构造
    return Sum::GetRes();
}

4. 友元 friend

4.1 友元函数

  1. 类内声明 friend 函数名,函数不是类成员;
  2. 可访问类私有/保护成员;不受访问限定符限制;
  3. 一个函数可作为多个类的友元。
cpp 复制代码
class A{ friend void func(A& a); };
void func(A& a){ cout << a._a; }

4.2 友元类

friend class B;:B类所有成员函数都能访问当前类私有成员

4.3 两大特性

  1. 单向:A是B友元 ≠ B是A友元;
  2. 不可传递:A友元B,B友元C ≠ A友元C;

4.4 优缺点

方便跨类访问,但破坏封装、提高耦合,尽量少用。

5 内部类(嵌套类)

5.1 定义

一个类写在另一个类内部,是独立类,外部类对象不包含内部类。

5.2 核心特点

  1. 内部类默认是外部类友元,可直接访问外部类私有;
  2. 内部类受外部类访问限定符控制(写private则外部无法使用);
  3. 调用格式:外部类::内部类 变量;
cpp 复制代码
class A
{
public:
    class B{}; // 内部类
private:
    static int _k;
};
A::B b;

5.3 使用场景

仅给外部类配套使用的工具类,封装隔离。

6 匿名对象

6.1 定义

类名(参数),无变量名,仅当前一行有效,行结束立即析构。

cpp 复制代码
A(1); // 匿名对象,执行完本行销毁
Solution().Sum_Solution(10); // 临时调用函数,用完释放

6.2 区分易错

A a(); 是函数声明,不是匿名对象;匿名对象不加变量名。

7 对象拷贝的编译器优化

7.1 优化目的

减少构造+拷贝构造连续调用,消除临时对象,提升效率。

7.2 典型优化场景

  1. 传参 f1(1):隐式转换直接构造,不生成临时对象;
  2. f1(A(2)):匿名对象直接传给函数,省去拷贝;
  3. 返回值 A aa2 = f2();:局部对象直接构造给接收变量,无临时拷贝;

7.3 不优化场景

aa1 = f2();:已有对象赋值,无法合并构造,会产生临时对象。

7.4 关闭优化

编译命令:g++ test.cpp -fno-elide-constructors

7.5 优化差异

  • VS2019 Debug:生成多层临时对象,多次拷贝;
  • VS2022 Debug/GCC:激进优化,直接构造目标对象,无临时拷贝。
相关推荐
我命由我123451 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
呜喵王阿尔萨斯1 小时前
extern “C“ in C++
c语言·c++·算法
深海呐2 小时前
仓颉语言是ArkTs的上层语言吗?就像kotlin和Java
java·开发语言·kotlin·仓颉
乐观勇敢坚强的老彭2 小时前
C++信奥静态数组和动态数组
数据结构·c++·算法
白露与泡影2 小时前
聊聊物联网Java后端的核心技术难点(水表/电表/充电桩实战)
java·开发语言·物联网
gumichef2 小时前
第一章:面向对象核心——类和对象深度讲解
开发语言·c++
冷咖啡离 我的笨笨2 小时前
数据去重:通过 C# 删除 Excel 中的重复行
开发语言·c#·excel
郝学胜-神的一滴2 小时前
[简化版 GAMES 104] 现代游戏引擎 04:从0到1构建你的游戏世界
c++·游戏·unity·游戏引擎·软件工程·unreal engine
Bryce学亮2 小时前
FileLine,基于 Qt 6 + QML 构建的跨平台文件传输与即时通讯工具
数据库·c++·人工智能·python·qt·github