类和对象——上篇

简单介绍

类的关键字是class,类与C语言中的结构体类似,可以在里面定义变量。但除此之外类还可以定义函数,功能较结构体更为丰富

类的组成

  1. 成员变量 :描述事物的属性(如姓名、年龄);
  2. 成员函数 :描述事物的行为(如说话、运行);
  3. 访问权限;
  • private 私有:仅类内部可访问(默认);
  • public 公有:外部也能访问;

示例:

cpp 复制代码
class Student {
// 公有成员
public:
    // 成员函数
    void setAge(int a);
    void show();
// 私有成员
private:
    int age;
 
};

成员函数在类里类外定义的区别

一、写法区别

1. 在类里面直接定义
cpp 复制代码
class Person{
public:
    // 类内直接写函数体
    void say(){
        cout << "hello";
    }
};
2. 类里声明,类外定义(注意要加域限定符)
cpp 复制代码
class Person{
public:
    void say();  // 只声明
};

// 类外加 类名::作用域限定
void Person::say(){
    cout << "hello";
}

二。核心区别(重点)

隐式内联
  • 类内定义默认自动变成 inline 内联函数
  • 类外定义 :默认不是内联,要手动加 inline 才是

类的声明和实例化

一、类的声明

类 = 图纸 / 模板 ,只是定义结构不占内存。如下图

cpp 复制代码
// 类的声明
class Student {
    // 成员属性
private:
    int id;
    string name;

    // 成员方法
public:
    void study();
    void setId(int x);
};

二、类的实例化(就是对象)

实例化 = 根据图纸造一个真实对象 ,会分配内存。如下

cpp 复制代码
// 实例化对象 s1
Student s1;
// 调用成员函数
s1.setId(101);
s1.study();

实例化中不同的对象其成员变量是不同的,但是他们的成员函数都是相同的 ,所以说在实例化时成员函数是不占空间的,而是被存在一个公共的区域。(插播:函数在编译后会形成一段指令,而函数指针就是第一段指令,而函数指针是不需要储存的,编译器在编译链接的时候就会找到函数的地址)

C++中的struct与class

C++ 里的 struct 和 class 几乎一模一样,唯一就是默认权限不同。

  • struct默认 public(成员默认公有)
  • class默认 private(成员默认私有)。

类的大小计算

前面说到,成员函数是不占空间的,所以说我们只需要计算成员变量的大小即可,这就与计算结构体的大小完全一样的。那么下面我们来复习一下就行。

内存对齐

在结构体中,变量在内存中并不是连续存储的。而是遵守一系列的内存对齐规则。

1 第一个成员变量在结构偏移量为0处。

2 其他成员变量要对其到对齐数的整数倍

3 对齐数等于min(编译器默认对齐数,该成员大小)

4 VS中的默认对其数是8

5 结构体的总大小为最大对齐数的整数倍(最大对齐数为默认对齐数和变量类型最大值的 较小值)

内存对齐的作用

一句话:提高CPU读取数据的效率

原因:CPU读取数据时不能从任意位置开始读任意个数据。

我们假设一次只能读取4个字节,从整数倍开始读,做一个示范。如图

CPU每次读四个字节,在内存对齐的方式中,只需要读一次就可以读取到int里的数据,而如果连续存储则需要两次才可以。

特别提醒:在类里面如果没有成员变量,那么他的大小是1;

内存划分为:栈(局部变量,函数栈帧,形参),堆(开辟空间),静态区(全局变量),常量区

this指针

是一个隐性的指针,在编译时编译器会自动加上,在形参和实参部分不能明显示地写出来。本质上就是对象的指针 ,如对于class Date他的指针是Date*const this,说明this指针的指向不能改变(区别于const Date* this 指针的指向可以改,但是不能修改指向的数据)

作用:

每个对象都有自己独立的 this 指针,成员函数靠 this 知道自己是哪个对象在调用

一道经典题目

下面这个程序的运行结果是:A编译错误(即语法上的错误)B运行奔溃(一般是越界,对空指针解引用)C正常运行

答案是C正常运行,因为成员函数并不在对象中存储,所以空指针也能够调用成员函数,而且该成员函数中没用使用到成员变量,所以是合法的(如果有对成员变量的使用就是非法的,因为A是空指针并没有实例化)

Stack在C和C++中的区别

在了解了上面的这些的C++的知识后,我们可以想一想C++对C语言做出了那些改进。我们就以Stack来作为例子来看一下

1 C++把数据和函数都放在了类里边,并用访问限定符进行了限定,这样就避免了外面对类里数据的修改,更加的安全,这也是封装的一种体现。

2 C++引入了引用的概念,传参更加方便,而且可以减少拷贝

3 C++中引入了缺省参数,使得我们在初始化时更加的方便(自动初始化,后面还会讲到)

4 由对象可以直接调用成员函数,并且会自动传this指针

类的默认成员函数

构造函数

构造函数是类的特殊成员函数创建对象时自动调用 ,专门用来**初始化对象成员,**相当于Iinit()。

构造函数的特性

  • 函数名和类名完全相同
  • 没有返回值 (连 void 都不写)
  • 创建对象时系统自动调用,不能手动主动调用
  • 可以重载(多个构造函数)

默认构造函数

更形象的名字可以叫做无参构造函数,即调用时不用传任何参数一般有3类:1 无参 2全缺省 3编译器默认生成的函数 这三者只能够同时存在一个,不然在调用时会有歧义。

注意:1当没有明显示地写构造函数的时候,编译器会自动生成,这个函数对内置类型不做处理,对自定义类型会调用他的默认构造

2 所以说在大部分时候我们都要明显示地实现构造函数

示例:

两种常用的调用方法:

析构函数

相当于Destroy(),完成对资源地清理释放工作。注意,针对地不是内置类型(int ,cahr),因为内置类型在在类的生命周期结束时就自动销毁了,无需Destroy()。所以析构函数针对地是像malloc,calloc开辟地空间地释放。

析构函数的特性

1 函数名是类名+~ 。如下图 Stack的析构函数名

cpp 复制代码
Stack~()

2三无:无参数,无返回值,无void

3 一个类只有一个析构函数,若未实现,系统会自动生成。

4对象生命周期结束时会自动调用析构函数

5我们不明显示的写析构函数时,编译器自动生成的析构函数会对内置类型不做处理,对自定义类型会调用他的拷贝构造,所以说像两个栈实现队列这种类可以不用写析构

6 无论我们是否明显示写析构函数,对于自定义类型都会调用他的析构

拷贝构造函数

是构造函数的重载,拿同类型的函数去构造(初始化)就叫拷贝构造。

拷贝构造函数的特性

1 第一个参数必须是类类型的引用(一般是const引用),用传值的方式会报错,因为会引发无穷递归调用(原因看下面第二点:会无穷调用拷贝构造,自己调用自己)

2 C++规定对类类型的拷贝必须调用拷贝构造,所以说类类型的传值传参,初始化,传值返回都会掉用拷贝构造

#3 (与构造函数和析构函数不同)当我们未明显示的写拷贝构造函数时,编译器自动生成的拷贝构造函数对内置类型会进行浅拷贝 (一字节一字节的拷贝),对于自定义类型会调用他的拷贝构造

4所以说对于没有空间资源的开辟的类我们无需明显示的写拷贝构造函数,由编译器自动生成的就足够用。而对于有空间资源开辟的类 ,我们需要自己明显示的实现拷贝构造函数进行深拷贝(即额外开辟空间,并拷贝值)。

为什么当有空间资源开辟时要进行深拷贝呢?因为自动生成的拷贝构造函数是一字节一字节地拷贝,这样就会出现两个指针指向同一个空间地情况,这样就有大问题了,比如会析构两次。

5 传值返回会产生一个临时对象(用于存储返回值,我前面地文章有说到过),会调用拷贝构造,总之一句话类类型地拷贝必须调用拷贝构造。如下图

拷贝构造的两种调用方法

如图的下面两行

运算符重载

介绍

  1. 定义: 对 C++ 已有的内置运算符+ - * / = << >> == 等),重新定义规则,让它能适配自定义类 / 结构体的运算。

  2. 本质: 运算符重载本质就是函数重载 ,把运算符当成特殊函数 operator

  3. 两种形式

  • 成员函数重载对象.运算符(参数)operator=operator==
  • 全局(友元)函数重载运算符(左操作数, 右操作数)operator<<operator>>

运算符重载的要点

1 优先级与结合性应与内置类型的运算符保持一致

2不能链接语法中没有的符号,构成新的运算符,比如:operator@

3**.*** (点星) :: (域作用限定符) ?: (三目运算符) . (点) 这四个运算符不能重载

4 重载操作符至少包含一个类参数,其实就是为了避免运算符重载对内置类型对象的改变,比如你重载了一个:int operator+(int a,int b),那你是何意味?

5 一个类需要重载那些运算符,具体要看运算符重载后有没有意义。比如日期类对象Date,日期+日期是没有意义的,而日期 - 日期是有意义的

赋值运算符重载

介绍

赋值运算符重载是类的一个默认成员函数,用于两个已经存在的对象之间的直接拷贝赋值**。区别于拷贝构造** 用于一个对像的拷贝去初始化一个要创建的对象。

拷贝构造和赋值运算符重载最根本的不同是:拷贝构造是用于构造的,用于初始化的,而赋值运算符是用于已有对象的拷贝。如下图的区分

赋值运算符重载的要点

1 赋值运算符重载是一个运算符重载,必须重载为成员函数(因为这是类的默认成员函数),参数建议写成const 引用,当然你写成传值传参也不会出错,只是会有拷贝的消耗

2有返回值,并建议写成当前类类型的引用,有返回值是为了连续赋值,引用是为了减少拷贝的消耗。

复习一下内置类型的连续赋值,如图

日期类运算符重载的示例:

3没有明显示地写赋值运算符重载时,编译器自动生成的会对内置类型完成浅拷贝,对自定义类型会调用他的赋值重载(与拷贝构造是类似的)。

4 所以编译器默认生成的赋值重载在没有空间资源开辟时是完全够用的,而当有空间资源开辟时我们就需要明显示的写赋值运算符重载了,有一个小技巧:当一个类需要析构的时候,那么他就需要明显示写赋值运算符重载

++运算符重载

++运算符重载时会遇到一个问题:前置++和后置++该如何区分?他们的函数名都是operator++,为了区分,C++规定后置++重载时增加一个参数int

一般来说前置++应该用的更多一些,因为前置++重载的时候不用额外定义新变量,消耗更少。

我们来看一下Date类型的前置++和后置++重载

<<和>>运算符重载

重载<<和>>必须重载为全局函数,因为重载为成员函数的时候this指针抢占了第一个形参的位置,而第一个形参为左操作数(操作数的顺序和形参的顺序是一致的),这样的话我们在调用这个函数时就变成(以日期类对象举例)d <<cout

这是不是倒反天罡了呀,这样不是说绝对不行,而是违反常理,不好。

<<>>流运算符是左到右结合的,区别于赋值运算符的从右到左结合,所以说返回值应当返回输入流/输出流的别名(C++中规定流只能传引用,无法拷贝)。

Date类型的<<运算符重载

那在类外的函数是怎么访问到类里面的私有成员变量的呢?变为公有的话就太挫了,加一个友元函数声明即可。(友元函数和友元类我会在类和对象下介绍)

返回输出流的原因

一般来说,有返回值都是为了能够连续使用。原理如下图

const成员函数

介绍

const修饰的成员函数称为const成员函数,const修饰成员函数放在参数列表的后边

const修饰的实际上是该函数隐含的this指针,表明该成员函数不能对成员变量做出修改

如Date类的成员函数被const修饰后,this指针会由Date* const this变成const Date*const this

使用const的场景

一般来说如果该成员函数不需要对成员变量做出修改,我们都尽量加上const

不然就可能发生权限扩大的问题,如下图,Print()函数没有加上const,那么他的this指针就是Date*const this 而d1是const的对象,发生了权限扩大

相关推荐
智者知已应修善业1 小时前
【51单片机独立按键和定时器中断的疑惑验证】2023-11-2
c++·经验分享·笔记·算法·51单片机
zzzsde1 小时前
【Linux】线程概念与控制(3):线程ID&&C++封装线程
linux·运维·服务器·开发语言·算法
消失的旧时光-19431 小时前
C 语言如何实现“面向对象”?—— 从 struct + 函数指针,到 Linux 内核设计思想
linux·c语言·开发语言
handler011 小时前
滑动窗口(同向双指针)算法:模板与例题解析
c语言·c++·笔记·算法·蓝桥杯·双指针·滑动窗口
Brilliantwxx1 小时前
【算法题】基础计算器的不同实现方式
c++·算法
Sunsets_Red1 小时前
P12375 「LAOI-12」MST? 题解
c++·算法·洛谷·信息学·oier·洛谷题解
小短腿的代码世界1 小时前
Qt时间日期处理与QTimer高级应用:从毫秒级精度到跨平台定时器的完整架构解析
开发语言·qt·架构
TAN-90°-2 小时前
Java 6——成员变量初始值 object equals和== toString instanceof 参数传递问题
java·开发语言
雪度娃娃2 小时前
多用户任务管理器
c++·个人开发