一、类的引入
在 C语言中,结构体内只可以定义变量,而在 C++ 中,将结构体升级为了类,既可以定义变量,又可以定义函数,但是 C++ 中,更喜欢用 class 来定义类
//C++ 兼容 C,并且将结构升级为类,可以在其中定义函数
struct A
{
int A;
void f()
{
cout << "struct A f()" << endl;
}
};
类也是新定义的一个域,内部访问内部时会在整个类中查找,不受限制,外部访问内部时需要用类名 + 域作用限定符指定访问
二、类的定义
用 class 关键字定义类,类中的变量 叫做 类的属性或成员变量,类中的函数 叫做 类的方法或成员函数
class 类名
{
//成员变量的声明
//...
//成员函数
//...
}; //和结构体一样,这里有分号
成员函数可以直接定义在类中,也可以定义在类外,直接定义在类中的成员函数,可能会被编译器识别为内联函数
写项目时通常需要将声明和定义分离:在 .h 文件中声明成员变量和成员函数,在 .cpp 文件中指定类中的函数进行定义
//A.h 文件
#pragma once
#include <iostream>
using namespace std;
class A
{
//声明成员变量
int a1;
int a2;
//声明成员函数 f1、f2
void f1();
void f2();
};
//A.cpp 文件
#include "A.h"
//类也是一个域,默认不会到类域中找
//如果定义的是类中的函数,需要显示指定,否则认为是定义在全局中的函数
void A::f1()
{
cout << "A f1()" << endl;
}
void A::f2()
{
cout << "A f2()" << endl;
}
为了避免下述情况,需要对成员变量名进行修饰,可以在变量名之前或之后加下划线 _,也可以采用其他方法
class Date
{
//成员变量的声明
int year;
int month;
int day;
//此时 Init 的参数便不能使用 year 这个名字,成员函数的参数取名变得麻烦了
//这种情况下,局部优先,使用的是参数 year
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
};
三、类的访问限定符
类的访问限定符是为了 可以选择性的将类中的成员提供给外部使用
通常将成员变量设置为 private,提供给外部使用的成员函数设置为 public
public(公开的):类中和类外都可以直接访问类中的成员
private(私有的):类中可以直接访问类中的成员,类外不可以直接访问类中的成员
protected(保护的):在以后学习继承时补充
访问限定符的作用域:
从该访问限定符的位置到下一个访问限定符之间,如果后面没有访问限定符,作用域到类的右花括号 }
C++ 中 struct 和 class 定义类的区别:struct 默认访问控制权限为 public(因为struct要兼容C),class 的默认访问权限为 private
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
四、类的实例化及对象的大小
类是一个自定义类型,用类这个类型 创建变量的过程,称为类的实例化,习惯上将这里的变量叫做对象
类实例化对象时可以省略关键字 class,struct 也可以定义类,因此 C++ 中结构体创建变量或实例化对象的时候都可以省略 struct
class Date
{
public:
int _year;
int _month;
int _day;
public:
void Init(int year, int month, int day)
{
//...
}
};
int main()
{
//两种方式都可以实例化对象,通常省略 class
class Date d1;
Date d2;
return 0;
}
类的实例化就是为类创建的对象开辟内存空间,对于类本身而言,就像整形 int 一样,是不占空间的
class Date
{
public:
int _year;
int _month;
int _day;
void Init(int year, int month, int day)
{
//...
}
};
int main()
{
//实例化对象开辟空间
Date d;
//Date._year; //error 类本身没有开辟空间
d._year = 2023; //对象有空间
return 0;
}
类中既有成员变量,又有成员函数,类实例化后的对象所占空间如何计算呢?
类可以实例化多个对象,每个对象的成员变量可能会存储不同的数据,但是调用的函数都是相同的
为了避免浪费空间(每个对象中都要存储相同的东西),于是 对象中即不会保存成员函数的代码,也不会保存成员函数的地址,而是 将函数的地址放到公共代码区(代码段),并用类域进行限制,函数的代码也放到公共代码区中
对象的大小即为所有成员变量的大小,但是成员变量需要满足 结构体内存对齐
#include <iostream>
using namespace std;
class demo
{
private:
char m1;
int m2;
int m3;
public:
void print()
{
cout << "class demo" << endl;
}
};
int main()
{
demo d;
//sizeof 计算类型创建的变量所占空间的大小,输出 12 12
cout << sizeof(d) << " ";
cout << sizeof(demo) << endl;
return 0;
}
空类 和 仅有成员函数的类 都没有成员变量,但是可以实例化对象,因此为了区别对象的不同,会为对象开辟一个空间
#include <iostream>
using namespace std;
//空类
class A1
{
};
//仅有成员函数的类
class A2
{
public:
void f()
{
cout << "class A2 f()" << endl;
}
};
int main()
{
//空类可以实例化对象
A1 a1;
//输出 1 1
cout << sizeof(a1) << " ";
cout << sizeof(A1) << endl;
//仅有成员函数的类,也可以实例化对象
A2 a2;
//输出 1 1
cout << sizeof(a2) << " ";
cout << sizeof(A2) << endl;
}
五、this 指针
首先创建一个 Date 类
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
和结构体一样,对象访问成员时,用 . 运算符,对象指针访问指向的对象的成员时,用 -> 运算符,也可以解引用对象指针后,用点运算符访问
int main()
{
Date d1;
Date* d2 = &d1;
//对象通过 .运算符访问成员
//输出 1970年1月1日
d1.Init();
d1.Print();
//对象指针通过 -> 运算符访问指向的对象的成员
//也可以先解引用后,再用 .运算符访问成员
//输出 2023年2月4日
d2->Init(2023, 2, 4);
(*d2).Print();
return 0;
}
既然对象中不存储函数的地址,对象调用成员函数时,应该指定访问,但是编译器会根据对象的类型,自动的将 d1.Init 解释为 Date:: Init去公共代码去的类域中找地址
d1 和 d2 在调用 Init、Print 成员函数时,编译器是如何知道成员函数中使用的 _year 等成员变量 是调用对象的成员变量呢?
编译器 在对象调用成员函数时,会隐式的将对象的地址作为参数传递,还会隐式的在成员函数的参数中用 (对象指针 const this)这个参数 来接收对象的地址,并且成员函数中的 _year都会被编译器解释为 this->_year,在成员函数中可以使用 this 指针
const 修饰 this,this 在成员函数中不能被修改指向
void Init(int year ...) 编译器处理为 void Init(Date* const this, int year ...)
void Print() 编译器处理为 void Print(Date* const this)
d1.Print() 编译器处理为 d1.Print(&d1)
d2->Print() 编译器处理为 d2.Print(d2)
在调用函数时,编译器需要为 this 指针传递调用成员的对象的地址,因此 Date.Init() 是错误的
注意:用户不能主动为成员函数中的 this 传参,也不能在成员函数中主动设置 this 参数,这些都只能编译器做
看如下代码,你认为会报错吗?
#include <iostream>
using namespace std;
class A
{
public:
void Init(int a = 1)
{
_a = a;
}
void Print()
{
cout << this << endl;
}
private:
int _a;
};
int main()
{
A* pa = nullptr;
pa->Init(); //运行崩溃
pa->Print(); //正常运行 输出 0000000000000000
(*pa).Init(); //运行崩溃
(*pa).Print(); //正常运行 输出 0000000000000000
return 0;
}
虽然 pa 是 nullptr,但是在对象调用函数时,编译器只做两件事
- 在 pa 所指对象的类中查找函数地址
- 传递 this 指针
因此只有当成员函数对 nullptr 进行解引用时,才会崩溃