SHARK_pssm头像
关注
【C++——类和对象(上)】封面图

【C++——类和对象(上)】

一、类

类是对一类事物的抽象描述,是模板、蓝图
类定义了一类事物有哪些属性、哪些行为

1.类定义格式

类的定义和结构体类似,但区别是类中可以定义函数

class Stack
{
    //成员变量(属性)
    //成员函数(方法)
};
  • class:定义类的关键字
  • Stack:类的名字
  • {}之间:类的主体
  • 类体内容统称为类的成员
    • 类中的变量:成员变量 / 属性
    • 类中的函数:成员函数 / 方法

Tips:类定义结束,大括号后面的分号千万不要忘记!

C++ 为了区分成员变量和函数形参,编码习惯上成员变量会增加标识: 比如变量前后加下划线_,或者以m开头,例如_year、m_year

写在类内部的成员函数,默认是 inline 内联函数

成员函数支持声明与定义分离:类内只写函数声明,函数实现写在类外部
由于外部属于全局作用域,全局函数只会在全局寻找,因此必须使用类名::指定所属类域

class Data
{
public:
    //类内声明
    void Init(int year,int month,int day);
};
//类外定义,必须加类域Data::
void Data::Init(int year,int month,int day)
{
    _year=year;
    _month=month;
    _day=day;
}

(1)struct定义类

C++ 中struct兼容 C 语言结构体用法,同时升级成为类,内部也可以定义成员函数。
日常开发中,我们优先使用class来定义类

//C语言写法
struct ListNodeC
{
    int val;
    struct ListNodeC* next;//C中struct+结构体名称代表类型
};

typedef struct ListNodeC1
{
    int val;
    struct ListNodeC1* next;
}LTNodeC;

//C++ struct,不需要重复写struct
struct ListNodeCPP
{
    int val;
    ListNodeCPP* next; 
};

2.访问限定符

封装是面向对象三大特性之一,访问限定符是C++的一种实现封装的方式。C++ 通过类把属性和方法整合在一起,再通过访问限定符选择性对外提供接口
访问限定符 { 在类外可以直接被访问: p u b l i c 在类外不能直接被访问: p r o t e c t e d 、 p r i v a t e 访问限定符\begin{cases} 在类外可以直接被访问:public\\ 在类外不能直接被访问:protected、private \end{cases} 访问限定符{在类外可以直接被访问:public在类外不能直接被访问:protected、private​

访问权限作用域: 从该限定符出现位置开始,直到下一个访问限定符出现;如果后面没有其他限定符,则作用域到类结束的}

定义的成员没有被访问限定符修饰:

  • class定义的类,默认访问权限为 private
  • struct定义的类,默认访问权限为 public

编码规范:
成员变量一般设置为private/protected做封装;需要外部调用的接口函数设置为public

3.类域

类定义了一个全新的作用域,类的所有成员都属于这个类域。
如果在类体外实现成员函数,必须使用::作用域解析运算符指明该成员属于哪一个类
类域的作用: 影响编译器标识符查找规则

二、实例化

实例化:使用类类型,在物理内存上创建对象的过程。

  • 对象:类的实例,根据模板生产出来的具体实体。
  • 关系:一个类可以实例化出多个对象

内存规则:
类只是模板,类里面的成员变量仅仅是声明,不会分配内存空间;
实例化出来的对象才会占用物理内存,用来存储成员变量。

1.成员变量和成员函数的存储

  • 成员变量: 每个对象各自独立,每个对象单独存储自己的成员变量。
  • 成员函数: 所有对象调用的是同一份函数代码,全部存放在公共代码区,避免内存重复浪费。

函数指针只是地址,不会存放在对象内存中;调用成员函数时,会被编译成汇编指令,编译器编译链接阶段直接找到函数地址

#include<iostream>
using namespace std;
class A
{
public:
    void Print()
    {

    }
};

class B
{

};

int main()
{
    A a;
    B b;
    cout<<&a<<endl;
    cout<<sizeof(a)<<endl;
    cout<<sizeof(b)<<endl;
    return 0;
}

a、b大小运行结果:
运行结果

运行程序后我们发现成员变量A、B类的大小不为0
空类: 没有任何成员变量的类
空类的大小为 1 字节。这 1 字节仅仅做占位,用来标识这个对象是唯一存在的,不存储有效数据

2.编译器编译成员函数

#include<iostream>
using namespace std;
class A
{
public:
    void Print()
    {
        cout<<"A::Print()"<<endl;
    }
};

int main()
{
    A* p=nullptr;
    p->Print();
    return 0;
}

成员函数地址编译阶段就已经确定,和对象本身没有关系;这里不会对空指针做解引用操作,因此p->Print();并没有因为解引用空指针报错

但语法上仍然要求通过对象 / 对象指针调用成员函数,两个原因:

  1. 需要通过对象类型确定对应的类域,编译器去类里面查找这个成员函数。
  2. 需要传递隐含的this指针作为函数参数。

三、this指针

不同对象调用同一个成员函数,函数能够区分当前操作的是哪一个对象,底层依靠隐含的this 指针
this指针: 编译器处理成员函数时,会自动在成员函数形参第一个位置增加当前类类型的指针。this 指针指向调用该函数的对象。

#include<iostream>
using namespace std;
class Date
{
public:
    //编译器底层等价于 void Init(Date* const this,int year,int month,int day)
    void Init(int year,int month,int day)
    {
        _year=year;   //等价 this->_year=year;
        _month=month; //等价 this->_month=month;
        _day=day;     //等价 this->_day=day;
    }

    //等价 void Print(Date* const this)
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    //编译器底层转换 d1.Init(&d1,2026,9,20);
    d1.Init(2026,9,20);
    //编译器底层转换 d1.Print(&d1);
    d1.Print();
    return 0;
}
  1. 在成员函数内部访问成员,本质全部是通过this->访问。
  2. ❗不能在实参、形参位置显式写 this;但是函数体内可以显式写this->。
  3. this指针本身是Date* const,指针本身不能被修改,但是指针指向对象的内容可以修改。
  4. this 属于函数形参,一般存放在栈帧;部分编译器会做优化,放到寄存器提高运行效率。

面向对象三大特性:封装、继承、多态
C++ 把数据和函数收纳到类中,通过访问限定符管控访问权限,这是封装的基础体现

四、类的默认成员函数

默认成员函数: 用户没有显式写,编译器会自动帮我们生成的成员函数
默认成员函数 { 初始化和清理 { 构造函数:主要完成初始化工作 析构函数:主要完成清理工作 拷贝复制 { 拷贝构造:使用同类对象初始化创建对象 赋值重载:主要是把一个对象赋值给另一个对象 取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现 默认成员函数\begin{cases} \text{初始化和清理}\begin{cases} \text{构造函数:主要完成初始化工作}\\ \text{析构函数:主要完成清理工作} \end{cases}\\ \text{拷贝复制}\begin{cases} \text{拷贝构造:使用同类对象初始化创建对象}\\ \text{赋值重载:主要是把一个对象赋值给另一个对象} \end{cases}\\ \text{取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现} \end{cases} 默认成员函数⎩ ⎨ ⎧​初始化和清理{构造函数:主要完成初始化工作析构函数:主要完成清理工作​拷贝复制{拷贝构造:使用同类对象初始化创建对象赋值重载:主要是把一个对象赋值给另一个对象​取地址重载:主要是对普通对象和const对象取地址,这两个很少会自己实现​

1.构造函数

构造函数是特殊成员函数,在对象实例化的时候自动调用,完成对象初始化,替代我们手动写的Init初始化函数

构造函数的特点:

  1. 函数名和类名完全相同
  2. 没有返回值,不能写 void
  3. 创建对象实例时,编译器自动调用
  4. 支持函数重载
#include<iostream>
using namespace std;
class Date
{
public:
    //无参构造
    Date()
    {
        _year=1;
        _month=1;
        _day=1;
    }
    //带参构造
    Date(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.Print();

    Date d2(2026,9,21);
    d2.Print();
    return 0;
}

⚠️注意:调用无参构造不要写括号!Date d1();这会被编译器识别成函数声明,编译器无法识别会报警告,不会创建对象

  • 如果我们没有写任何构造函数,编译器自动生成一份无参默认构造;只要用户显式实现任意构造函数,编译器就不再生成默认构造

默认构造函数:不需要传递实参就可以调用的构造函数。
包括三种:

  1. 无参构造函数
  2. 全缺省构造函数
  3. 编译器自动生成的无参构造

Tips:这三者有且只能存在一个,不能同时共存

C++ 类型划分:

  • 内置类型(基本类型):语言原生类型,int、char、double、指针等
  • 自定义类型:class /struct 我们自己写的类

编译器自动生成的默认构造行为:

  1. 内置类型成员: 没有强制初始化要求,值随机,看编译器实现。
  2. 自定义类型成员: 自动调用该类型的默认构造函数初始化;如果该类没有默认构造,直接编译报错,此时需要初始化列表解决。

默认的无参的显式初始化需要传参数、如果有内置类型需要我们自行处理…所以构造函数大多数情况下都需要自己实现,小部分类似MyQueue、Stack有默认构造情况下可以依靠编译器自动生成

大部分场景构造函数建议自己实现,遵循「构造函数应写尽写」

2.析构函数

析构函数和构造函数功能相反,对象生命周期结束的时候自动调用,完成对象资源释放清理,等价于之前数据结构写的Destroy销毁函数

析构函数的特点:

  1. 析构函数名是在类名前加上字符~,函数名:~类名
  2. 无返回值、无参数
  3. 一个类只能有一个析构函数,不手动写编译器自动生成默认析构
  4. 对象生命周期结束,系统自动调用
  5. 编译器生成默认析构:内置类型成员什么都不处理;自定义类型成员会自动调用它的析构函数。
  6. 就算自己手写析构,类里面的自定义类型成员依然会自动调用析构

编码判断:

  • 类内部没有向堆申请资源(如Date 日期类): 不需要手写析构,使用编译器默认生成即可。
  • 默认生成的析构可用(如MyQueue): 不需要手写析构
  • 类中有 malloc/new 申请堆内存资源(如Stack 栈类): 必须手动写析构释放资源,否则内存泄漏
class Stack
{
public:
    Stack(int n=4)
    {
        _a=(int*)malloc(sizeof(int)*n);
        if(nullptr==_a)
        {
            perror("malloc fail");
            return;
        }
        _capacity=n;
        _top=0;
    }

    ~Stack()
    {
        free(_a);
        _a=nullptr;
        _top=_capacity=0;
    }

private:
    int* _a;
    int _top;
    int _capacity;
};

int main()
{
    Stack st1;
    Stack st2;
    return 0;
}

如果有多个对象,对象销毁顺序:后定义的对象先析构

3.拷贝构造函数

拷贝构造是特殊的构造函数: 使用一个已经存在的同类对象,去初始化创建另一个新对象

拷贝构造函数的第一个参数必须是自身类类型的引用,且其余参数要有缺省默认值

#include<iostream>
using namespace std;
class Date
{
public:
    Date(int year=1,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=day;
    }

    //拷贝构造函数
    Date(const Date& d)
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
    }

    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

拷贝构造关键特点

  1. 拷贝构造属于构造函数的重载。
  2. 第一个参数必须是类的引用,不能传值!因为语法逻辑上拷贝构造传值传参会引发无穷递归调用
  3. C++ 规定:自定义类型发生拷贝行为,必须调用拷贝构造。对象传值传参、传值返回,都会调用拷贝构造。
  4. 用户不显式写拷贝构造,编译器自动生成默认拷贝构造:
    • 内置类型:逐字节拷贝(浅拷贝 / 值拷贝)
    • 自定义类型成员:调用该类型的拷贝构造函数。

Tips:参数建议加上const保护源对象不被修改

引发无穷递归调用原因:
C++规定传值传参要调用拷贝构造
传值传参要调用拷贝构造,调用拷贝构造要先传参,如果拷贝构造完成了再继续调用原来的函数
但是如果本身调用的就是拷贝构造,调用这个拷贝构造要传值传参,传值传参会形成一个新的拷贝构造,就会造成无穷递归

但是如果第一个参数是类类型的引用:
引用传参,传入的参数是别名,不会形成新的拷贝构造

#include<iostream>
int main()
{
	Date d1(2026,9,20);//普通构造
	d1.Print();
	Date d2(d1);//拷贝构造
	d2.Print();
	//两种写法,都会调用拷贝构造
	return 0;
}

值拷贝/浅拷贝: 按字节复制,一个字节一个字节的拷贝
深拷贝: 不仅拷贝对象本身,对象指向的堆资源也要重新开辟一份拷贝

如果一个类显式实现了析构并释放资源,那么它就需要显式写拷贝构造,否则就不需要

⚠️Tips:
如果类中管理堆资源(Stack 栈),默认生成的浅拷贝会让多个对象指向同一块堆空间;对象析构时,同一块内存会被 free 多次,程序直接崩溃,这种情况必须手写拷贝构造,实现深拷贝

Stack 深拷贝示例:

Stack(const Stack& st)
{
    _a=(int*)malloc(sizeof(int)*st._capacity);
    if(nullptr==_a)
    {
        perror("malloc fail");
        return;
    }
    memcpy(_a,st._a,sizeof(int)*st._top);
    _top=st._top;
    _capacity=st._capacity;
}

调用拷贝构造:

#include<iostream>
int main()
{
	Stack st1;
	st1.Push(1);
	//方式一
	Stack st2(st1);
	//方式二
	Stack st3=st1;
	return 0;
}

传值返回:

Stack func1()
{
	Stack st;
	return st;
}
int main()
{
	Stack ret=fun1();
	return 0;
}

传值返回会产生一个临时对象调用拷贝构造。传值引用返回返回的是返回对象的别名(引用),没有产生拷贝

传引用返回局部对象:

Stack& func2()
{
	Stack st;
	return st;
}
int main()
{
	Stack ret=func2();
	return 0;
}

如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,这时的引用相当于一个野引用,类似一个野指针

传引用返回可以避免拷贝,前提:返回的对象生命周期不会随着函数结束销毁

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/SHARK_pssm/article/details/163024908

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--