首先要知道类和C语言的结构体很像,都是定义出一个新的类型,结构体只能定义变量,而类不仅可以定义变量,还可以定义函数。
要注意的是在C++中struct
兼容C语言的所有用法,同时把struct
升级为类,C++用class
定义类。
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类的定义也有两种方式:
1️⃣声明和定义全部放在类中
class Person
{
public:
//打印基本信息
void Print()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
private:
char* _name;
char* _sex;
int _age;
};
2️⃣声明和定义分开
//.h
class Person
{
public:
//打印基本信息
void Print();
private:
char* _name;
char* _sex;
int _age;
};
//.cpp
void Person::Print()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
要注意一点:有{}
说明是个域,要用::
访问
暂时把
protected
和private
看成一样的,public
就是在类外面能直接被访问,而private
在类外面不能被直接访问
当不写的时候class的默认访问权限为private,struct为public(因为struct要兼容C)
【面试题】
C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是struct的成员默认访问方式
是private。
面向对象的三大特性:封装、继承、多态。
封装:类和对象都封装到类中,想被访问的定义成公有,不想被访问的定义成私有。
封装的本质是管理
封装更加严格,规范,不封装更自由,对使用的人素养要求高。
来看一个类:
class Date
{
public:
void Print()
{
cout << _year << _month << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
cout << sizeof(d) << endl;
return 0;
}
最后编译的结果是 12
我们可以看到光private内的三个变量就达到了12,那么类中的成员函数为什么不算在内?
假设我们实例化了两个对象d1,d2,他们的三个成员变量不一定相同,但是他们的Print函数却相同,如果给每个对象都在内存中存函数的地址,势必会造成空间浪费。所以在计算大小时不管成员函数
计算成员变量的大小也遵循内存对齐规则结构体/联合体大小的计算
class A
{
//类中仅有成员函数
public:
void f()
{}
};
//空类
class B
{};
int main()
{
A a;
B b;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
return 0;
}
两个的结果都为 1
为什么不是0?
给了一个字节不是为了存储数据,是为了占位,表示对象存在过
不然无法区分B b; B bb; B bbb;
这三个对象。当我们看这三个对象的地址会发现他们不一样。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 1, 1);
Date d2;
d2.Init(2022, 1, 2);
return 0;
}
我们知道d1和d2两个对象初始化调用和的都是同一个函数,那么为什么调用的同一个函数,却能完成各自的初始化呢?
看似init函数有三个参数,而编译器会增加一个隐含参数
Init(Date* this, int year, int month, int day)
init内部可以写成:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
那么在主函数内:
Date d1;
d1.Init(2022, 1, 1);//d1.Init(&d1, 2022, 1, 1)
Date d2;
d2.Init(2022, 1, 2);//d2.Init(&d2, 2022, 1, 2)
this指针特性:
1️⃣this指针是隐含的,编译时增加的,不能自己在函数的调用和定义中加
2️⃣可以在成员函数中使用
【面试题】
下面程序能运行通过吗?
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
答案是
p->PrintA()
不能运行成功(空指针),而p->Show()
可以
p->PrintA()
可以看作p->PrintA(&p)
,而题中函数的_a
可以看作this->a
,就是空指针解引用。而p->Show()
不会出出现这种情况
先定义出个日期类:
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
类里面的成员函数我们什么都不写,编译器也会默认生成6个函数,这六个函数叫做默认(缺省)成员函数
构造函数完成的是对象的初始化
构造函数的存在是为了防止我们忘了使用自己定义的初始化函数,而直接去使用对象发生错误
特性:
1️⃣ 函数名与类名相同。
2️⃣ 无返回值。
3️⃣ 对象实例化时编译器自动调用对应的构造函数。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
当我们对象实例化后它会自动调用Date函数
4️⃣ 构造函数可以重载。
5️⃣ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参
构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
能够重载说明了有多种初始化方式
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date()
{
_year = 2022;
_month = 1;
_day = 1;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 9);
Date d2;
return 0;
}
d1 调用的是有参数的构造函数,d2 调用的是无参的构造函数。
而我们可以把两个重载函数合二为一,写成全缺省参数。
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
要注意一种情况:
当有参数的构造函数写成全缺省的时候,d2实例化就会出错,虽然语法上没什么问题,但是编译器却不知道该调哪个函数。
6️⃣ 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
输出结果:
-858993460年-858993460月-858993460日
我们可以看到,好像编译器默认生成的构造函数好像什么事都没做
class A
{
public:
A()
{
_a = 0;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
_aa.Print();
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这里编译器把_aa的值初始化为0。所以得出结论:
编译器默认生成的构造函数,对内置类型(如int,char,指针等等)默认不初始化,对自定义类型(如class,struct)编译器会调用他们的默认构造函数初始化。
默认构造函数:
有很多人认为默认构造函数就是我们没写,编译器默认生成的函数,这种理解是不全面的
1️⃣ 我们不写,编译器默认生成的
2️⃣ 我们自己写的无参的
3️⃣ 我们自己写的全缺省的
用一句话来说就是:不用参数就可以调用的构造函数
总结:
默认函数大多数都是要自己写的,并且建议写成全缺省的形式
首先明白一点:析构函数不是完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特性:
1️⃣ 析构函数名是在类名前加上字符 ~。
2️⃣ 无参数无返回值。
3️⃣ 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4️⃣ 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date
{
public:
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
其实在日期类里面,析构函数没有起到什么作用,写不写都一样,那么在什么情况下析构函数有用呢?
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
我们在数据结构中学到的栈,每次都要写Destroy
函数,而在C++中我们就不用自己写了。像Stack类析构函数具有重大意义。
5️⃣ 先构造的后析构,后构造的先析构
int main()
{
Stack s1;
stack s2;
return 0;
}
对象是定义在栈中,函数调用会建立栈帧,栈帧中的对象构造和析构也符合后进先出的规则。
s1先构造
s2后构造
s1后析构
s2先析构
析构函数的跟构造函数类似,对内置类型和自定义类型处理方式不同,内置类型不处理,自定义类型会处理。
总结一下:
当你的类在构造函数执行过程中申请了一些资源(内容空间),需要在对象被销毁时进行释放时,就需要自己定义析构函数。
拷贝构造就跟字面意思一样,比方说实例化了一个日期类对象d1,现在想要实例化个跟d1信息一样的对象,就要用到拷贝构造。
int main()
{
Date d1(2022, 7, 9);
Date d2(d1);
return 0;
}
那么我们就来看看拷贝构造怎么实现?
特性:
1️⃣ 拷贝构造函数是构造函数的一个重载形式。
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
验证发现这个代码编不过,这就要看第二个特性:
2️⃣ 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
为什么上面的函数会无穷递归呢?
而如果用引用:
从次可得:
函数传参,如果传的是自定义类型的对象,推荐使用传引用。如果使用传值,就会调用拷贝构造。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3️⃣ 若未显示定义,系统生成默认的拷贝构造函数。它会按照字节序拷贝,我们称作浅拷贝(值拷贝),就是对内置类型浅拷贝,
总结一下:
拷贝构造函数是特殊的构造函数,涉及到深浅拷贝,像Date这样的类就需要浅拷贝,不需要自己写,但是像Stack这种的类就需要自己写,不然会导致对象析构两次,导致崩溃。
C++为了增强代码的可读性引入了运算符重载,而赋值运算符重载就是运算符重载中的一种。
在以前我们如果要判断两个日期对象是否相等时:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool Equal(const Date& d1)
{
return this->_day == d1._day
&& this->_month == d1._month
&& this->_year == d1._year;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 9);
Date d2(2022, 7, 9);
cout << d1.Equal(d2) << endl;
return 0;
}
这样我们就面临一个问题:代码可读性差。
如果我们能写成d1 == d2
是不是就能通俗易懂了呢?
C++就可以用运算符重载来实现,使用关键字operator
//定义在全局,注意把成员变量设成公有
bool operator==(const Date& d1, const Date& d2)
{
return d1._day == d2._month
&& d1._month == d2._month
&& d1._year == d2._year;
}
而我们比较时就有两种写法(两种一样):
operator==(d1, d2);
d1 == d2;
但是如果我们不想变成公有呢?
那么就写成成员函数:
bool operator==(const Date& d1)
{
return this->_day == d1._day
&& this->_month == d1._month
&& this->_year == d1._year;
}
比较用法:
d1.operator==(d2);//d1.operator(&d1, d2);
d1 == d2;//d1.opreator(&d1, d2);
其他运算符重载方法相同,例如+ - * /
注意:
1️⃣ 不能通过连接其他符号来创建新的操作符:比如operator@
2️⃣ 重载操作符必须有一个类类型或者枚举类型的操作数
3️⃣ 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4️⃣ 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5️⃣.*
、::
、sizeof
、? :
、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符重载也是拷贝行为,不一样的是,拷贝构造是实例化一个对象时,那同类对象的值拷贝。而赋值运算符重载是在拷贝时两个对象都已经存在且已经被初始化过,进而把一个对象赋值给另一个对象。
Date d1(2022, 7, 9);
Date d2;
d2 = d1;
这样就可以完成拷贝赋值
那么我们怎么能自己实现呢?
void operator=(const Date& d)
{
this->_day = d._day;
this->_month = d._month;
this->_year = d._year;
}
但是这么实现不太好,因为无法连续赋值(d1 = d2 = d3)。
//返回值不加&又会调拷贝构造
Date& operator=(const Date& d)
{
this->_day = d._day;
this->_month = d._month;
this->_year = d._year;
return *this;
}
总结一下:
编译器默认生成的赋值运算符跟拷贝构造函数的特性一样,就是说像Date这种类我们自己不用写。对自定义类型,会去调他的赋值运算符重载完成拷贝。
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
假如我们想调用比较是否相等的运算符重载函数但是写错了:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return (this->_day = d._day)
&& (this->_month = d._month)
&& (this->_year = d._year);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 10);
Date d2(2022, 7, 10);
d1 == d2;
return 0;
}
就变成了赋值运算符重载了。那么我们有没有什么方法可以避免呢?
我们可以给this
指针也加上const
:
因为this是隐含的,所以就有以下这种方法:
bool operator==(const Date& d) const//修饰*this
{
return (this->_day = d._day)//error
&& (this->_month = d._month)//error
&& (this->_year = d._year);//error
}
const权限问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
1 不可以
2 可以
前两个好理解,重要的是后面两个。
3 不可以
void Fun() const
{
Print();
}
void Print()
{
cout << _year << endl;
}
这里可以看成Fun的this传递给Print(),权限放大,不可以。
4 可以
void Fun()
{
Print();
}
void Print() const
{
cout << _year << endl;
}
这个实际上是Fun的this传递给Print(),权限缩小,可以。
这里本质就是要看指针的传递。谁调用就是谁传递给别人。
这两个操作符不重要,了解就行。
取地址操作符重载:
Date* operator&()
{
return this;
}
const取地址操作符重载:
const Date* operator&() const
{
return this;
}
像我们前面的构造函数初始化使用的是函数体内初始化,
初始化列表初始化也可以完成初始化。
Date(int year = 0, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,不要忘记最后的{}。
使用的时候两种方法都可以使用,并且可以混着使用:
Date(int year = 0, int month = 1, int day = 1)
: _year(year)
, _month(month)
{
_day = day;
}
注意:
1️⃣ 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2️⃣ 类中包含以下成员,必须放在初始化列表位置进行初始化:
① 引用成员变量
② const成员变量
③ 自定义类型成员(该类没有默认构造函数)
class A
{
public:
//不是默认构造函数(需要传参)
A(int x)
: _x(x)
{}
private:
int _x;
};
class Date
{
public:
Date(int year = 1)
: _year(year)
, _n(1)
, _p(year)
, _a(0) // 显示调用
{
//不能在函数体内初始化,只能使用列表初始化
//_n = 1;
//_p = year;
}
private:
int _year;
//引用必须在定义时初始化,但此处是声明,所以没问题
int& _p;
const int _n;
A _a;
};
3️⃣建议尽量使用初始化列表初始化,因为初始化列表是对象定义的地方,就算不写也会使用初始化列表初始化(随机值)。
4️⃣ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
return 0;
}
成员变量初始化顺序是按照声明的顺序走的,先初始化_a2,但此时_a1还没被初始化,所以_a2是随机值。然后用a初始化_a1,_a1的值为1。
补充一个知识点:
有时候我们会看到A(3);
这种构造方式,他是构造匿名对象,它的生命周期只在这一行中,这一行走完就会调用析构函数。
那么什么情景下会用到匿名对象呢?
要使用一个对象,但只在这一行有用,就可以用匿名对象,方便快捷。
单参数的构造函数,支持隐式类型转换。
class A
{
public:
A(int a)
: _a(a)
{}
private:
int _a;
};
int main()
{
A a = 2;//相当于先构造一个临时变量 A tmp(2);
//再拷贝构造 A a(tmp);
return 0;
}
而现在的编译器进行了优化,相当于直接调构造函数:A a(2)
但是如果我们不想让它隐式类型转换,就可以用explicit
关键字:
class A
{
public:
explicit A(int a)
: _a(a)
{}
private:
int _a;
};
如果再想A a = 2;
就会报错
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
class A
{
public:
private:
static int _n;
};
//必须在类外边定义,加上类域,不写static
int A::_n = 1;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
这个的结果为 1,说明_n是存在静态区的,属于整个类,也属于类的所有对象。
使用场景:
计算程序中A定义出了多少个对象?
class A
{
public:
A()
{
++_n;
}
A(const A& a)
{
++_n;
}
//所有的对象都是构造或者拷贝构造出来的!!
int GetCount()
{
return _n;
}
private:
//要注意这里是声明,他的初始化要在类外面。
static int _n;
};
int A::_n = 0;
int main()
{
A a1;
A a2;
A a3;
cout << a1.GetCount() << endl;
return 0;
}
static int GetCount()
{
return _n;
}
他跟成员函数的区别是没有this指针,不能访问非静态成员。
静态成员和静态成员函数都不属于某个对象,突破类域就能访问(也要看访问限定符)。
总结:
1️⃣ 静态成员为所有类对象所共享,不属于某个具体的对象
2️⃣ 静态成员变量必须在类外定义,定义时不添加static关键字
3️⃣ 类静态成员即可用类名::
静态成员或者对象.静态成员来访问
4️⃣ 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5️⃣ 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
我们知道编译器生成的默认构造函数对内置类型默认不初始化,所以在C++11中引入了解决办法:
class A
{
public:
private:
int _a = 0;
int* _p = nullptr;
};
我们没有写构造函数,内置类型也会初始化。要注意的是:
这里不是初始化,依旧是声明,0 和 nullptr为缺省值,如果我们自己写构造函数(不是默认构造函数),没给参数就用缺省值初始化。
我们发现前面调用Print()函数的操作比较复杂,那么我们能不能使用operator<<
来让我们输出?
查C++库可以知道cout的类型是ostream
void operator<<(ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
但是当我们用cout << d1
调用时候却发现出现错误。
//cout << d1
//第一个参数是左操作数,第二个是右操作数,两个操作数反了!!
void operator<<(ostream& out)
//void operator<<(Date* this, ostream& out)
所以必须写成 d1 << cout
,但是不能满足我们的需求。
所以我们要写到全局才能调整顺序。
//全局
void operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
}
但是写到全局又会出现访问权限的问题。
这里就引入友元函数:
class Date
{
//友元函数的声明
//返回值为ostream是为了连续输出
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
说明:
1️⃣ 友元函数可访问类的私有和保护成员,但不是类的成员函数(无this指针)
2️⃣ 友元函数不能用const修饰(无this指针)
3️⃣ 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4️⃣ 一个函数可以是多个类的友元函数
5️⃣ 友元函数的调用与普通函数的调用和原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Date;//前置声明
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void PrintTime()
{
// 直接访问时间类私有的成员变量
cout << _t._hour << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
这里我们想在Date类里面调用PrintTime()函数去访问时间类的私有成员变量,明显不可行,但是当我们让Date变成Time的友元类后就可以了。
class Time
{
// 声明日期类为时间类的友元类,
//则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
注意:
1️⃣ 友元关系具有单向性,没有交换性。
Date是Time的友元类,那么就可以在Date中直接访问Time的私有成员。但是Time却不能访问Date。
2️⃣ 友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
一般情况下不建议使用友元,因为会破坏封装。
友元函数不属于任何一个类,是定义在类外的普通函数,所以不能访问私有成员
而友元类的成员函数都可以访问另一个类的非公有成员
就是在一个类的里面再定义一个类。
而内部的类天生是外部的类的友元。
class A
{
public:
//B叫做A的内部类
class B//B天生是A的友元
{
private:
int _b;
};
private:
int _a = 0;
int* _p = nullptr;
};
B可以直接访问A。
特点:
1️⃣ 内部类可以定义在外部类的public、protected、private都是可以的。
2️⃣ 注意内部类可以直接访问外部类中的static、枚举成员,不需要
外部类的对象/类名。
3️⃣ sizeof(外部类)=外部类,和内部类没有任何关系