目录
一、类的基础
1.类 class/struct
类可以理解为c语言中结构体的升级版,在类中不仅仅能够存储数据了,还可以写函数,在类中我们称之为成员变量(数据)和成员函数(函数), 一个类可以实例化出N个对象,类名即可表示对象的数据类型,class/struct + 类名 { ... };即为类的表示(注意最后有一个分号)
类内都有访问限定符:public,protected,private
public修饰的在类外可以直接访问,protected和private不可以,这两个更多的区别会在后面讲到,class的默认访问限定符是private,而struct默认的是public,所以明显class的封装性是更好的,更加安全,所以我们一般习惯使用的是class更多
class A //类A
{
//默认private,在最上面修饰可以不说明
int a;
int b; //成员变量
...
public:
void ptint() { ... } //成员函数
...
protected:
...
private:
... //放在下面要强调是private的
};
A a1; //一个类A的对象a1
在声明成员变量的时候可以给缺省值,int day = 1;
2.类外定义
成员函数可以在类的外面再定义,类内先声明,但是在类外定义要注意用域作用限定符标注函数的作用域
class Stack
{
public:
void Init(); //声明
};
void Stack::Init() //定义
{
...
}
3.对象的实例化
类内的成员都属于声明,没有实例化,比如上面的那个类Stack,需要 Stack st;此时才属于定义出了一个成员对象
计算一个类的大小:sizeof(st) <=> sizeof(Stack), 二者等价,可以是对象名或者类名
在类的大小中,成员函数不算入栈帧内存大小,成员变量才算,类中的成员函数定义完之后就是有一个固定的地址,每个类对象调用的成员函数地址都是一样的,因为如果类内每个函数都存会造成很大的浪费
可以发现空类的大小为1,而且成员函数的内存确实不计入类的内存大小
4.隐含的this指针
在类内有一个隐含的this指针,指向的是成员对象,eg. d.Print(); <=> d1.Print(&d);
1.this指针在实参和形参是不可以显示的写出来的,编译器自己添加
2.在类内可以显示的使用this指针
#include <iostream>
using namespace std;
class A{
int n = 1;
public:
void Print(){
cout << this->n << ' ' << n;
}
};
int main(){
A a;
a.Print();
return 0;
}
结果:1 1
由调试结果可知this指的对象就是a,二者的成员变量是一样的
3.this指针的储存地址是在栈上,因为this也是个形参
二、类的6个默认成员函数
1.构造函数
1.函数名和类名相同
2.无返回值,不写void
3.成员对象实例化时自动调用
4.可以函数重载
class Date
{
public:
//无参的构造函数
Date()
{
_day = 1;
}
//有参的构造函数
Date(int day)
{
_day = day;
}
//全缺省的构造函数
Date(int day=1)
{
_day = day;
}
private:
int _day;
};
一般在构造函数内初始化成员变量,如果没有初始化,成员变量则为随机值,无参构造函数和全缺省构造函数同时出现时会报错,因为有歧义,所以不可以同时存在
成员对象在实例化时,编译器会自动匹配对应的构造函数
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_day = 1;
cout << "Date()" << endl;
}
Date(int day)
{
_day = day;
cout << "Date(int day)" << endl;
}
private:
int _day;
};
int main()
{
Date a;
Date b(10);
return 0;
}
结果:Date()
Date(int day)
在没有手搓的构造函数时,编译器会自动生成无参构造函数
可见是可以的,但是绝大多数情况下都有初始化的需求所以都是需要手搓的
2.析构函数
1.形式:~类名
2.无参数,无返回值
3.对象生命周期结束时,自动调用,完成对象中的资源清理
4.不写时编译器会自动生成
#include <iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << "~Date()" << endl;
}
private:
int _day;
};
int main()
{
cout << "1 ";
Date a;
cout << "2 ";
return 0;
}
结果:1 2 ~Date()
这段代码中可以看出来析构函数的调用是在出了主函数,成员对象a要销毁时才调用的
3.拷贝构造函数
1.是构造函数的一种重载
2.参数为类类型对象的引用
3.不写时编译器会自动生成
#include <iostream>
using namespace std;
class Date
{
public:
Date(int day)
{
_day = day;
}
//拷贝构造函数
Date(Date& d)
{
_day = d._day;
}
private:
int _day;
};
int main()
{
Date a(10);
//下面两个等价,都可以,都属于拷贝构造
Date b(a);
Date c = a;
return 0;
}
当类类型的成员对象作为实参传递给形参,也就是传值传参的时候,会调用拷贝构造
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{ }
Date(Date& d)
{
cout << "Date(Date& d)" << endl;
}
void func(Date d)
{
}
private:
int _day;
};
int main()
{
Date a;
Date b;
b.func(a);
return 0;
}
结果:Date(Date& d)
清楚了传值传参会调用拷贝构造后,我们就可以来看为什么一定要用类类型的引用???
下面会给出一段代码,从这里可以知道为什么
void func(Date d)
{
...
}
Date d(10);
func(d);
这段代码中,成员对象d作为func函数的实参传过去的时候,会调用拷贝构造,而拷贝构造的形参又是一个类类型对象传值,Date(Date d),接下来就是无穷递归,所以必须要传址传参
以上的拷贝构造,为浅拷贝,是按字节拷贝的,但是在遇到栈的情况下会遇到问题,比如array1和array2同时要拷贝一个空间,那这个被拷贝的空间就会析构两次,导致程序崩溃,所以面对这种情况我们需要深拷贝,也就是去开一个一样大的空间,然后一个一个传值,下面的代码就是深拷贝的一个例子
Stack(const Stack& st)
{
_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
memcpy(_array, st._array, sizeof(DataType) * st._size);
_size = st._size;
_capacity = st._capacity;
}
4.运算符重载
形式:返回值 operator 重载符号 (参数)
1.参数个数看操作符的操作数,要注意类内的成员函数有this这个隐含的参数
2.返回值看需求,eg.比大小返回bool,日期类相减返回天数int
3.不能创建语言外的新符号,比如@
4.重载操作符必须有一个类类型的参数,int operator-(int, int)就不可以,因为编译器本来就有
5.最好不要修改符号本身的含义,比如“-”实现“+”,除非有需求
6.不能重载的5个运算符:sizeof,
:: 域作用限定符,
?:条件运算符,
. 点
.* 点星
7.必须重载为成员函数的4个运算符:= 赋值
[ ] 下标运算符
( ) 括号
-> 箭头
class Date
{
int _year;
int _month;
int _day;
public:
bool operator != (Date d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return false;
}
return true;
}
};
赋值运算符重载
一个已经存在的对象,拷贝赋值给另一个已经存在的对象
Date d1; Date d2; d2 = d1; 两个对象都要是已经存在的
区分:
拷贝构造:Date d1; Date d2 = d1; 这里d2是原本不存在的对象,是一个已经存在的去赋值给一个要创建初始化的对象,这个是拷贝构造
1.返回值为类类型或引用,这样便于连续赋值,d1 = d2 = d3,从右往左,d2 = d3的返回值是d2,然后再d2赋值给d1,如果是void的话就不能连续赋值
2.不写时编译器会自动生成
Date& operator= (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
关系运算符重载
多运用复用思想,比如先重载 < 和 == ,就可以用这两个写 <=,再重载 >
bool operator <= (const Date& d)
{
if (*this < d || *this == d)
{
return true;
}
return false;
}
bool operator > (const Date& d)
{
return !(*this <= d);
}
运算符+不会改变自己,是产生新的对象,所以返回值是类,而+=是改变自己,所以返回值是类的引用,可以用+=复写+
多个同一运算符重载可以构成函数重载
Date& operator - (int day)
{
d1 += day;
//表达的含义返回为日期d1在day天之后的日期
return d1;
}
int operator - (const Date& d)
{
int ret = d1 - d;
//表达的含义为两个日期之间差多少天,返回值为天数,是整形
return ret;
}
ps:但是一般两个日期差值用小日期++和大日期判断,整形变量计数方便一点
int count = 0;
while (max != min)
{
++min;
++count;
}
前置运算符和后置运算符
为了能够更好的区分前置和后置,强制规定前置运算符无参数,后置运算符带一个参数int,这个int无意义,但是必须要写
Date& operator++() //前置
{
*this += 1;
return *this;
}
Date operator++(int) //后置
{
Date tmp(*this);
*this += 1;
return tmp;
}
因为后置返回的是++之前的值,所以要用一个tmp储存起来,然后返回,因为返回的是一个临时对象,所以是传值返回
流输入和流提取
1.内置类型可以直接使用,但是自定义类型就需要重载
2.返回值为istream/ostream,用void不太好,也是一样为了便于连续输入
3.建议重载成全局函数或者友元函数,不然的话形参里面就会首先先默认占据一个隐含的this指针,在主函数内的顺序就变成 d << cout 了,非常的不符合逻辑
class Date
{
int _year;
int _month;
int _day;
public:
friend istream operator >> (istream& in, Date& d);
friend ostream operator << (ostream& out, const Date& d);
};
istream operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream operator << (ostream& out, const Date& d)
{
out << d._year << ' ' << d._month << ' ' << d._day << endl;
return out;
}
5.取地址重载 和 const取地址重载
1.取地址重载可以获取对象的地址,const取地址重载获取const对象的地址
2.不写时编译器会自动生成
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()
{
return this ;
}
private :
int _day ;
};
三、其他
1.友元函数和友元类
友元函数:friend + 函数声明
1.这个是声明,友元函数的定义在类内的话直接定义,在类外的话不用加friend
2.友元函数的参数没有隐含的this指针
3.友元函数可以访问成员对象的私有成员变量
class Date
{
public:
friend void Print(Date d) //类内定义
{
cout << d._day;
}
private:
int _day = 1;
};
//类外定义
//void Print(Date d)
//{
// cout << d._day;
//}
友元类:一个类中可以作为另一个类的成员变量,friend + class + 类名,就是友元类的声明
1.友元类可以访问外部类的成员
2.但是外部类不可以访问友元类的成员
class Time
{
friend class Date;
...
};
Date就可以访问Time的成员,Time不可以访问Date的
2.静态成员变量
关键字:static
1.存在于静态区,不存在于对象中
2.声明时不能给缺省值
3.必须在类外定义
class A
{
private:
int a1;
int a2;
static int count;
//不存在于对象中 -> sizeof(A)=8
};
int A::count = 0;
3.初始化列表
1.形式:构造函数名 :成员变量1(参数),成员变量2(参数),......
初始化列表是每个对象中成员定义的地方
2.初始化列表的初始化顺序是按照类内成员变量声明的顺序来的,不是按照列表成员变量出现的顺序的
3.初始化列表的本质是调用的默认构造函数
4.初始化列表和函数体内初始化可以混用
5.在成员变量声明时给的缺省值就是在初始化列表调用的,由于静态成员变量不在对象中,所以不会走初始化列表,也解释了静态成员变量为什么不可以在声明时给缺省值
6.const和引用&必须写在初始化列表处,因为这两个必须在定义时就初始化
Date(int year, int month, int day): _year(year), _month(month)
{
_day = day; //可以混用
}
4.内部类
1.类内的类就是内部类,仅仅受到类域和访问限定符限制,不影响外部类的size
2.内部类天生为外部类的友元
3.内部类是外部类的私有
class A
{
class B;
};
A a1; //可以
B b1; //不可以
A::B b2; //可以
5.匿名对象
1.匿名对象具有常性
2.匿名对象的生命周期只有创建的那一行,使用完立马销毁
class A
{
...
};
A(2); //匿名对象
A a(1); //有名对象
6.构造对象时的一些优化
1.类类型作为形参的时候,如果传值传参,会有两次构造两次析构,一次在传过去的时候会创建一个临时对象,一次在传返回值的时候又会创建一个临时对象,而引用的传址传参的话就只有一次,可以减少拷贝
2.全局对象是在main之前进行构造,析构则相反,在main之后
3.局部静态变量是在第一次使用的时候初始化,不使用则不会初始化