目录
- 前言
- 一、类的定义
- 二、类的访问限定符及封装
- 2.1访问限定符
- 2.2封装
- 三、类的大小
- 3.1为什么需要内存对齐
- 3.2为什么成员函数不占用类的内存?
- 3.3为什么空类的大小是1个字节?
- 四、this指针
- 4.1this指针的引入
- 4.2this指针的特性
- 五、类的6个默认成员函数
- 5.1构造函数
- 5.1.1构造函数的特性
- 5.2析构函数
- 5.3拷贝构造函数
- 5.4复制运算符重载
- 5.5const成员函数
- 5.6 取地址及const取地址操作符重载
前言
今天是少年正式认识"对象"的第一天,虽然此"对象"非彼对象,但是少年也想好好的与你认识认识,所以少年在这里先跟你打个招呼。
我们都知道豹子、老虎、猫…都是猫科动物。这里我们用猫科动物来作为这些动物的类,而豹子、老虎、猫是这个类下的具体动物即对象。在生活中有了类与对象的概念我们就可以很简单描述一个复杂的事物。比如:王者荣耀是一个我方英雄与敌方英雄的塔防游戏。英雄是类,塔也是类,用了两个类就简单的描述了几百万人玩的游戏。试想一下假如没有类的概念你该怎么描述呢?
而在C++中为了更好的"表达"也引入了类与对象的概念(祖师爷666)。
一、类的定义
C++中使用关键字 class 来定义类, 其基本形式如下:
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Hero
{
public:
//英雄的1技能
int func1()
{
return 0;
}
//英雄的2技能
int func2()
{
return 0;
}
//英雄的3技能
int func3()
{
return 0;
}
//英雄的4技能
int func4()
{
return 0;
}
private:
//英雄名字
char name[20];
//英雄血条
int Hp;
};
2.类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
Tower.h文件
#pragma once
class Tower
{
public:
//塔的攻击功能
int Attack();
private:
//塔的名字
char name[20];
//塔的血量
int Hp;
//塔的伤害值
int DamageValue;
};
Tower.cpp文件
#include"Tower.h"
int Tower::Attack()
{
return 0;
}
一般情况下,更期望采用第二种方式。好处现在可能不太明显,后面代码量上去后可能自己就有了体会,特别是还加了继承与多态。
二、类的访问限定符及封装
2.1访问限定符
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
2.2封装
C++中面向对象的特性有三:封装、继承、多态。学到这里代表少年面向对象的思想刚刚开始入门了。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。假设你是王者荣耀的实现团队的leader,你希望你的团队写出在英雄放技能时可以修改英雄固定的属性值的代码吗?可能的不行的。
通过需要访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用就是封装。
三、类的大小
前面在学习C语言的时候知道了结构体大小的计算,这里面主要涉及到内存对齐,同样类的大小计算也要涉及到内存对齐。比如:
那么为什么要有内存对齐规则呢?上次在C语言里没有细说,这次借助C++的类在这里展开一下。
3.1为什么需要内存对齐
- 首先不是所有的硬件平台都能访问任意地址上的数据。
- 其次某些硬件平台只能只在某些地址访问某些特定类型的数据,否则抛出硬件异常,及遇到未对齐的边界直接就不进行读取数据了。
- 最后为了代码的可移植性,和提升CPU访问内存的效率,所以结构体一定要内存对齐。本质:空间换区时间的做法。
cpu的内存由于硬件的原因是一块一块的,块的大小可能是2字节、4字节、8字节取决于硬件,因此cpu在读取内存时是一块一块的读取的。不可以随机访问某一个地址,但是可以随机访问某个倍数倍的地址。比如:
最后对比一下对齐与不对齐的读取效率。如图:
3.2为什么成员函数不占用类的内存?
话不多说直接上图:
通过2张图可以发现类的大小是真的与类内的成员函数无关。但是这是为什么呢?这主要与类的存储方式有关。如图:
成员函数不像成员属性那么特殊(每个对象的属性是独一无二的),成员函数就像是对象的方法你想用直接去公共代码区调用就好,不用每个对象都"带着"这样浪费空间。
3.3为什么空类的大小是1个字节?
直接上图:
按道理应该是0呀?为什么是1呢?假设是0字节大小,即不在内存上存储。那么少年问你,对一个类取地址这个操作犯毛病吗?应该是合理的哟,但是它都不在内存上了它的地址是多少呢?所以为了出现这么的情况,编译器给了一个字节的空间来存储这个类,来证明这个类存在过。
四、this指针
4.1this指针的引入
思考一下下面的代码为什么会是这样的打印结果呢?
根据上面的分析d1与d2都应该是调用的同一个函数(下图可以证明),而且函数貌似也没有传参呀。为什么打印结果会不同呢?
其原因是:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
注:this不能在形参和实参显示传递,但是可以在函数内部使用且this指向不能改。所以严格意义上是这样的。
4.2this指针的特性
问题1:this指针存放在哪?
由上图可知this指针是个形参,所以存放在栈区。
问题2:this指针可以为空吗?回答这个问题前先做这样一道题:
this指针可以为空,但是注意不要在成员函数内对它解引用。
学到这里我们也就可以理解为什么A::Print();
这样写法不行。因为编译器不知道传个什么东西给this指针。
五、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
5.1构造函数
5.1.1构造函数的特性
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。比如:
#include<iostream>
using namespace std;
class Data
{
public:
Data()
{ }
Data(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()
{
Data d1;//调用的是无参构造函数
Data d2(2022,12,12);//调用的是有参构造函数
d1.Print();
d2.Print();
return 0;
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year=2022;
int _month=12;
int _day=12;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。