前言
最后一节主要是对类和对象的收尾和补充。
如果想看前面两节,请点:
1.运算符重载
-
前置++和后置++重载
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
前置++是返回+1后的结果,这里this指向的对象函数结束后不会销毁,故使用引用方式返回来提高效率 。
//后置++
Date operator++(int)
{
Date tmp(*this);
_day += 1;
return tmp;
}
因为后置++与前置++都是一元运算符,这里为了构成正确的函数重载 ,C++规定在后置++重载时多加了一个int类型的参数,但是在调用时该参数不用传递由编译器自动传递。这里是先使用再+1,故需要先把this保存一份,然后再对this+1,最后返回先前复制的临时对象,采用的是值返回故不能加&。
2.const成员函数
当我们创建了一个const对象时,想再去调用类中的函数则会报错,
class A
{public:
void Print()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
const A a;
a.Print();//err
return 0;
}
这里就需要使用const成员函数,在函数的后面加上const
const修饰*this,this的类型变成const A* 。
const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。所以以后在内部不改变成员变量的成员函数最好加上const,这样const对象和普通对象都可以调用。
3.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成。
4.再谈构造函数
-
构造函数体赋值
我们在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。 如下,
Date(int year, int month, int day)
{
_year = year;//第一次
_year = 2020;//第二次
_month = month;
_day = day;
}
-
初始化列表
初始化列表:以一个 :(冒号) 开始,接着是一个以 ,(逗号) 分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1,1,1);
return 0;
}
类中包含以下成员,必须放在初始化列表位置进行初始化:
- const成员变量
class WQ
{
public:
int q1;
int q2;
const int q3;
};
int main()
{
WQ w;
return 0;
}
这是因为const成员变量必须在定义是就初始化,所以为了不报错有2种处理方法:
还有一种就是使用初始化列表 ,初始化列表就是所有成员变量定义的位置,不管是否显示初始化列表写,那么编译器每个变量都会在初始化列表定义初始化。
class WQ
{
public:
WQ()
:q3(0)
,q2(2)
{
q1++;
q2++;
}
private:
int q1 = 1;
int q2 = 1;
const int q3;
};
int main()
{
WQ w;
return 0;
}
我们来调试一下上面的代码看一下,
首先我们要知道,当我们写了初始化列表后,哪怕我们没有将变量显示的写在初始化列表里面,但都会对这些变量进行初始化。当我们给了变量缺省值又在初始化列表里面初始化了值,这时这个变量的值按初始化列表里面的,如果我们没有在初始化列表里面写的变量在初始化的时候会直接使用缺省值来初始化,所以上面的结果便可以明白原理了 。
再看下面这串代码,猜一下结果
class WQ
{
public:
WQ()
:q(2)
,w(q)
{
w++;
q++;
}
private:
int w = 1;
int q = 1;
};
int main()
{
WQ w;
return 0;
}
调试一下,
这里为什么w会变成随机值呢?其实变量初始化的顺序是根据定义时的顺序来的,当程序走到初始化列表先对w进行初始化,这里w的初始化值给的是q,但此时q还是一个随机值便导致w初始化成了随机值。其实这里自己手动调试,感受最为直观。
- 引用成员变量
class A
{
public:
A()
:a1(1)
, rt(a1)
{
}
private:
int a1;
int a2;
int& rt;
};
- 自定义类型成员(且该类没有默认构造函数时) (默认构造函数是指不用传参数或参数缺省的构造函数)
class B
{
public:
B(int b)
{
cout << "B()" << endl;
}
private:
int b;
};
class A
{
public:
A()
:a1(1)
,bb(0)//
{
}
private:
int a1;
int a2;
B bb;//
};
int main()
{
A a;
return 0;
}
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
explicit关键字
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造参数,还具有类型转换的作用。接收单个参数的构造函数具体表现:
- 构造函数只有一个参数
- 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
- 全缺省构造函数
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A(int a1, int a2)
:_a(a1)
{
}
private:
int _a;
};
int main()
{
A aa1(1);//构造函数
A aa2 = 1;//隐式类型转换--构造+拷贝+优化——>构造
return 0;
}
单参构造函数,没有使用explicit修饰 ,具有类型转换作用;当加上explicit修饰后,将会禁止构造函数的隐式转换
在C++11加入了多参数构造函数调用方式
class A
{
public:
A(int a1, int a2)
:_a(a1)
{
}
private:
int _a;
};
int main()
{
A aa2(1, 1);
A aa3 = { 1,1 }; //C++11
return 0;
}
5.static成员
声明为static的类成员成为类的静态成员,用static修饰的成员变量,称为静态成员变量;用static修饰的成员函数,称为静态成员函数。静态成员变量一定要在类外初始化
那么应该这样初始化
class A
{
public:
A()
{}
private:
static int a1;
static int a2;
};
//初始化
static int a1 = 0;
static int a2 = 2;
int main()
{
A a;
return 0;
}
静态成员特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只声明
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
那么就可以用stctic成员来解决下面这个问题:实现一个类,计算程序中创建出了多少个类对象
但是我们在初始化时缺遇到了这样的问题
会报命名冲突,发现count也是一个库函数 这里因为我在开头将std域全部展开所致,那为了避免这个问题们以后在初始化时可以限定域 int A::count = 0; ,正确代码:
class A
{
public:
A(int a)
{
count++;
}
A(const A& a)
{
count++;
}
static int GetCount()
{
return count;
}
private:
static int count;
};
int A::count = 0;
int main()
{
A a1(1);
A a2(a1);
A a3 = 1;
cout << A::GetCount() << endl;
return 0;
}
6.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
友元函数
现在我们尝试去重载operator<< ,会发现没办法将operator<<重载成成员函数
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1, 1, 1);
cout << d1;
return 0;
}
发先无法通过编译
当我们进行修改后,便可以正常运行
但是,这种书写方式不符合常规的调用。是因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧。
所以,为了解决这个问题,可以将operator<<重载写成全局函数,但我们又遇到了新的麻烦,全局函数无法访问类的成员,此时就需要用友元来解决(operator>>同理)。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
那么,真正的operator<<和operator>>重载是这样写的
class Date
{
//友元函数
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, 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;
return out;
}
//运算符重载>>
istream& operator>>(istream& in, Date& d)
{
in >> d._year;
in >> d._month;
in >> d._day;
return in;
}
int main()
{
Date d1;
cin >> d1;
cout << d1;
return 0;
}
友元函数的说明
- 友元函数可以访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员。
class Date
{
//友元类
friend class Time;
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
class Time
{
public:
Time(int year, int month, int day)
:time_year(year)
, time_month(month)
,time_day(day)
{}
void Get_Date()
{
_d._year = time_year;
_d._month = time_month;
_d._day = time_day;
}
private:
int time_year;
int time_month;
int time_day;
Date _d;
};
int main()
{
Date d1;
Time t1(2, 2, 2);
t1.Get_Date();
return 0;
}
友元类的说明
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Date类中声明Time类为其友元类,那么可以在Time类中直接访问Date类的私有成员变量,但想在Date类中访问Time类中私有的成员变量则不行。
- 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
7.内部类
先看下面代码,猜一下结果是多少呢?
class A
{
public:
class B
{
private:
int _b;
};
private:
int _a;
};
int main()
{
A aa;
cout << sizeof(aa) << endl;
return 0;
}
那是因为内部类是一个独立的类只是受到域的限制,所以当求A的类大小时,只虚关注A的类本身的成员变量大小即可。
内部类的概念:
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类天生就是外部类的友元函数
class A
{
public:
class B
{
public:
void Print_A(const A& a)
{
cout <<a._a<< endl;
}
private:
int _b;
};
private:
static int _a;
};
int A::_a = 2;
int main()
{
A::B b;
b.Print_A(A());
return 0;
}
内部类的特性
- 内部类可以定义在外部类的public、protected、private都是可以的 。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
内部类可以作为简单了解,因为在C++中不习惯使用内部类的书写方式。
结语
到这里,C++的类和对象也就结束了。好消息是,我们已经入门了;坏消息是,这才只是入门的第一关。C++的学习是一个起点偏高后期呈缓式上升的难度,所以这就意味着学习入门阶段的知识像是在练基本功,只有把基本功练扎实了再去学后面的知识才能更快的掌握。最后送给大家也送给自己的一句话:“慢一点,才更快”。