面向过程和面向对象初步认识
面向对象程序设计以对象为核心,该方法认为程序由一系列对象组成。 类是对现实世界的抽象,包括表示静态属性的数据和对数据的操作,对象是类的实例化。 对象间通过消息传递相互通信,来模拟现实世界中不同实体间的联系。 在面向对象的程序设计中,对象是组成程序的基本模块,面向过程"(Procedure Oriented)是一种以过程为中心的编程思想。 这些都是以什么正在发生为主要目标进行编程
总结:
面向过程关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
面向对象的关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; //分号不能丢
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
C++类型定义的两种方式
1、声明定义放在一起
//声明一个动物类
class animal
{
//成员函数
void _sleep()
{
printf("早睡早起\n");
}
void _catch()
{
printf("猎杀时刻\n");
}
//成员变量
int age;
int height;
int weight;
};
2、声明定义分开处理
class animal
{
//成员函数
void _sleep();
void _catch();
//成员变量
int age;
int height;
int weight;
};
//指定_catch函数和_sleep属于animal类域,定义声明分开时这样处理
void animal::_catch()
{
printf("猎杀时刻\n");
}
void animal::_sleep()
{
printf("早睡早起\n");
}
类的访问限定符
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- class的默认访问权限为private,struct为public(因为struct要兼容C)
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是
private。
类的基本概念
面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互,封装本质上是一种管理
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域
//当类的成员函数声明和定义分开时,需要指定该函数在哪个类域中
class animal
{
//成员函数的声明
void func();
};
//指定该函数在animal类域中声明过
void animal::func()
{
//代码
}
类的实例化
1、用类类型创建对象的过程,称为类的实例化
2. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
3. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
重点:this指针
class Date
{
public:
void showDate()
{
cout << _year << "-" << _month << "-" << _day<< endl;
}
//实际上的SetDate函数是这样的:
//void SetDate(Date *this, int year, int month, int day)
void SetDate(int year, int month, int day)
{
_year= year;
_month= month;
_day= day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
//设置对象的成员变量值,由于类成员变量已经被私有化了
//,要想在外界访问只有通过成员函数访问,成员函数是公有的
d1.SetDate(2021, 10, 27);
d2.SetDate(2020, 3, 27);
//打印值
d1.showDate();
d2.showDate();
return 0;
}
通过SetDate函数去初始化对象成员变量,其实编译器在处理的时候实际上会多添加一个参数这个参数是一个指针类型,他叫this,在传参的时候会将对象的地址传递给this,这样this就指向了这个对象,这样子就能通过this去修改该对象里面成员变量的值,既然是隐藏的,this可以加,但不推荐
打印this的值和打印对象的地址是一样的,他们共属于一个东西
总结及补充:
该类对象去调用该类成员函数时,通常会将对象的地址传递过去,而this就是指向这个对象的,通过this去修改对象的成员属性
补充:
1、this指针是隐含的,是编译器编译时加的,不能显示的在调用和函数定义中加
2、可以在成员函数中使用this
3、this一般是存在栈上的,不同的编译器不同,vs使用的是ecx寄存器存储传参的
这里将对象的地址d1存储到ecx寄存器中,通过使用vs2013反汇编观察到的现象
进一步加强对this的理解
以下程序会崩溃吗?
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();程序崩溃,因为对象本身是一个指针类型,但是却是空指针,在调用成员函数的时候,将空指针传递给this,this再去访问该对象的成员变量,就会有空指针解引用行为,程序会崩,p->Show();正常运行,因为并没有去访问该类对象的成员变量,并且成员函数的地址并不会存放到对象里面,所以是完全ok的
类的6个默认成员函数
默认成员函数的概念:我们不写,编译器自动生成的成员函数就叫默认成员函数
构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。构造函数是特殊的成员函数,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
class Date
{
public:
//带参数的构造函数
Date(int year, int month, int day);
//不带参数的构造函数
Date();
private:
int _year;
int _month;
int _day;
};
//类中声明,类外定义,指定是哪个类域
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//
Date::Date()
{
}
构造特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
注意:
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成,默认构造函数是不会去初始化对象的成员的
如果使用编译器提供的默认构造函数,对象成员的值就会是随机值
显示定义构造函数
class Date
{
//不指定参数,显示定义构造函数,编译器不再提供
Date()
{
//代码
}
//一般推荐带参数的构造函数,并提供缺省参数
Date(int year = 2021, int month = 10, int day = 28)
{
//代码
}
};
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
析构函数
析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类对象的一些资源清理工作
class Date
{
Date(int year = 2021, int month = 10, int day = 28)
{
//代码
}
//无须提供参数不带返回值,自动被调用完成对象的资源清理
~Date()
{
//代码
}
};
析构函数特征
析构函数是特殊的成员函数。
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 编译器默认生成的析构函数,对于内置类型成员不会处理,而对于自定义类型成员会处理
- 同样的析构函数跟构造函数也是类似,如果我们不写编译器会自动提供默认的析构函数,如果我们写编译器就不再提供默认的析构函数
析构函数的析构顺序
先进后出,后进先出,符合栈的特性
class Date
{
public:
Date()
{
cout << this << "对象创建" << endl;
}
~Date()
{
cout << this << "对象析构 " << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
Date d2;
Date d3;
return 0;
}
观察到的现象是先创建的对象后析构,后创建的对象先析构
拷贝构造函数
拷贝构造函数也属于类的六个默认构造函数:只有单个形参,该形参是一个类型对象的引用,一般常用const修饰(加const的目的:只做拷贝,不做修改),用已经存在的对象拷贝构造出一个新的对象,完成拷贝构造对象的初始化
特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 拷贝构造函数也是属于默认成员函数,我们不写编译器自动生成拷贝构造,这个拷贝构造会对内置类型完成浅拷贝,或者值拷贝
再学习浅拷贝之前,我们先学习拷贝构造函数在哪些场景会存在问题
1、如果是值传递的方式调用拷贝构造函数会存在一定的隐患,会陷入无限死循环,
正确写法,必须传递引用,使用引用的方式是为了给给对象取别名,调用拷贝构造出一个新的对象,传递引用并不会存在死循环
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//自定义拷贝构造函数,引用传递
Date(Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void Dateshow()
{
cout<< _year << " " << _month << " " << _day << endl;
}
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2021 ,10 ,30);
//调用拷贝构造函数完成对d2对象的初始化
Date d2(d1);
d1.Dateshow();
d2.Dateshow();
return 0;
}
补充:针对于自定义类型推荐传递引用,值传递会调用拷贝构造函数,也是会存在一定的消耗的
浅拷贝带来的隐患举例
如果是用一个类创建出来两个对象,这两个对象的成员中都含有一个指针,并且它们都是指向同一块空间的,当这两个对象被销毁时编译器自动调用析构函数,这样子一来同一块空间就会被释放两次,解决浅拷贝的办法就是深拷贝,后序再来讲解
总结:
浅拷贝的问题,调用析构函数时,同一块空间被释放两次
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表
注意:
- 1、不能通过连接其他符号来创建新的操作符:比如operator@ 2、重载操作符必须有一个类类型或者枚举类型的操作数
3、用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4、作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5、.* 、::、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现
内置类型本身是支持运算符的,而自定义类型默认不支持,而C++可以用运算符重载来让类对象支持某个运算符
这里举例两种运算符重载方式,目的是为了让读者知道其中的意义在哪,先看在类中重载运算符
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//等号运算符重载
bool operator==(Date d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2021, 10, 29);
Date d2(2021, 10, 29);
Date d3(2021, 10, 30);
//比较两个对象的大小
bool ret = d1.operator==(d2);//true
return 0;
}
operator==(Date d2),C++的一个运算符重载语法,operator==表示的是函数名,后面的括号里面带的是函数的参数,是一个类类型的形参,该函数的功能是将两个对象进行比较,将比较的结果值返回,当然这里是布尔类型,这里同样也是值传递
调用处会将d1的地址传递给this,那么this就指向这个对象d1,而对象d2是通过值拷贝的方式过去的,那么就可以对这两个对象的成员进行比较,最后将结果返回
再来看第二种方式,他的设计并不像第一种方式那么灵活,反而看起来很生硬
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year; // 年
int _month; // 月
int _day; // 日
};
//类外重载运算符比较两个对象的大小
bool operator==(Date d1, Date d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2021, 10, 29);
Date d2(2021, 10, 29);
Date d3(2021, 10, 30);
//bool ret = d1.operator==(d3);
bool ret = d1 == d2;//true
return 0;
}
对比这两种写法,当然在类外重载后使用起来会更简洁,但是就破坏了封装性,相比之下在类中重载代码管理起来也方便,并且还利于类的封装性,其实编译器在内部也会将d1 == d2 转换为d1.operator==(d3);,所以在实现上还是推荐第一种写法
重载[ ]运算符
class Array
{
public:
//调用构造函数完成对象的初始化
Array()
{
_numsize = 10;
for (int i = 0; i < _numsize; i++)
{
_arr[i] = i ;
}
}
//获取_numsize
inline int& length()
{
return _numsize;
}
//重载[]运算符,返回数组索引下标的位置
inline int& operator[](int pos)
{
return _arr[pos];
}
private:
int _arr[100];
int _numsize;
};
int main()
{
Array arr;
//循环取出对象成员数组的值并打印
for (int i = 0; i < arr.length(); i++)
{
cout << arr[i] << endl;
}
return 0;
}
通过operator[](int pos) 重载运算符后对象arr无需再使用 . 去访问成员而是通过重载[ ]后直接对成员访问,更为便捷,当然函数体比较小,加上内联更合适
拷贝复制运算符
看下面这个例子,将一个对象d1赋值给另外一个对象,在这个时候编译器会自动调用我们重载过后的 operator=拷贝赋值运算符,将d1对象的值拷贝赋值到d2对象
class Date
{
public:
//自定义拷贝构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
cout << "调用了拷贝构造函数:Date(Date& d)" << endl;
}
//重载赋值运算符,使自定义类型也能像内置类型一样使用操作符
Date operator=(const Date& d1)
{
_year = d1._year;
_day = d1._day;
_month = d1._month;
cout << "operator=()" << endl;
//返回值带回去的时候会将this
//这个对象拷贝构造出一个临时对象,最后将临时对象带回去
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 10, 30);
Date d2 = d1;
return 0;
}
需要注意的是这里会产生一个临时对象,因为重载的operator=它的返回值是一个对象,并不是引用,值返回会多调用一次拷贝构造函数,从打印的结果可以看出
可以再改善一下,引用加上防止多调用一次拷贝构造函数
Date& operator=(const Date& d1)
{
//如果对象不是自己本身就对他处理,否则不需要,自己拷贝自己没意思
if(this != &d1)
{
_year = d1._year;
_day = d1._day;
_month = d1._month;
}
cout << "operator=()" << endl;
//返回值带回去的时候会将this
//这个对象拷贝构造出一个临时对象,最后将临时对象带回去
return *this;
}
补充:
赋值运算符也是一个默认成员函数,我们不写编译器会默认生成一个,编译器默认生成赋值运算符跟拷贝构造的特性是一致的。
1、针对内置类型也会完成浅拷贝
2、针对自定义类型,他会调用它的赋值运算符重载完成拷贝
总结一下编译器默认生成的成语函数:
1、构造和析构的特性是类似的,我们不写编译器对内置类型不做处理,自定义类型调用他的构造函数和析构函数
2、拷贝构造和赋值重载特性是类似的,内置类型会完成浅拷贝,自定义类型会调用它们的拷贝构造和赋值重载运算符
学了重载运算符后,简单实现一个日期类,进一步加深对重载运算符的理解
日期类的实现
类声明:
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day);
//日期加天数/减天数
Date operator+(int day);
Date& operator+=(int day);
Date operator-(int day);
Date& operator-=(int day);
//自增
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
//日期关系比较
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator!=(const Date& d);
bool operator==(const Date& d);
//日期加减日期
int operator-(const Date& d);
//显示日期
void showDate();
private:
int _year;
int _month;
int _day;
};
类成员函数实现:
inline int GetMonthDay(int year, int month)
{
static int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = arr[month];
//四年一润,百年不润,四百年一润
if (month == 2 && year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
{
day = 29;
}
return day;
}
针对于频繁调用的函数加上内联,防止频繁开辟栈帧,由于每次开辟一个栈帧这个栈帧都会去创建一个数组,必然是会有消耗的,所以数组改成静态的更适合
Date::Date(int year, int month, int day)
{
//检查年月日是否是合法的
if (year >= 0 &&
month > 0 && month < 13 &&
day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
}
加减法运算符重载
创建日期类对象的构造函数,需要做一定的检查,防止出现逻辑错误
Date& Date::operator-=(int day)
{
//如果day是负数的情况
if (day < 0)
{
//负负得正
_day += -day;
//处理不合法的天
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
}
//day是正数的情况
else
{
//合计天数,天满了向月借,月满了向年借
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
}
//返回处理后的日期对象
return *this;
}
重载operator-=运算符,使非法日期变得合法,加法进位思想
优化
但是实现这么一个重载运算符难免会觉得代码长,进一步优化让他简短,提高我们程序的复用性
Date& Date::operator-=(int day)
{
//如果day是负数的情况
if (day < 0)
{
//复用operator+=接口
*this += -day;
}
//day是正数的情况
else
{
//合计天数,天满了向月借,月满了向年借
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
}
//返回处理后的日期对象
return *this;
}
重载operator+=运算符
类似于之前的代码,加法进位思想
Date& Date::operator+=(int day)
{
//对可能会存在非法的时间处理
if (day < 0)
{
_day += day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
}
else
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
}
return *this;
}
同样的重载operator+=运算符也是,需要区分正负数,如果day存在负数的情况可能会存在错误,实际上if (day < 0)这一小块也可以复用之前的operator-=
Date& Date::operator+=(int day)
{
if (day < 0)
{
//复用operator-=运算符
*this -= -day;
}
else
{
//处理日期,使日期变得合法
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
}
//返回处理后的日期对象
return *this;
}
前置++和后置++都是完成一个自增的操作,区别是自增前和自增后,前置++返回的是自增后的值,而后置++返回的是自增前的值,++完了才自增,它们的运算符是一样的,函数名就是一样的,所以为了同时存在,这里会使用到函数重载,这里使用int占位,实际上这个占位符并没有多大作用,只是为了取到函数重载的意义
//前置++
Date& Date::operator++()
{
//复用operator+=运算符
return *this += 1;
}
//后置++
Date Date::operator++(int)
{
//复用operator+=运算符
Date tmp(*this);
*this += 1;
//tmp出了作用域就销毁了,会调用拷贝构造函数创建一个临时对象,最后返回临时对象
return tmp;
}
前置- -和后置- - 运算符重载,跟前面的类似,不做细讲
//前置--
Date& Date::operator--()
{
//复用operator--运算符
return *this -= 1;
}
//后置--
Date Date::operator--(int)
{
//复用operator--运算符
Date tmp(*this);
*this -= 1;
//返回拷贝构造出来的临时对象
return tmp;
}
关系运算符重载,比较两个日期类对象的大小
逻辑简单,复用处理就行
bool Date::operator>(const Date& d)
{
//年大比年
if (_year > d._year)
{
return true;
}
else
{
//月大比月
if (_month > d._month)
{
return true;
}
//月相等比天
else if (_month == d._month)
{
if (_day > d._day)
{
return true;
}
}
}
//都不满足就是假
return false;
}
bool Date::operator==(const Date& d)
{
//年月日都相同
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator>=(const Date& d)
{
//复用> 和==重载后的运算符
if (*this > d || *this == d)
{
return true;
}
else
{
return false;
}
}
bool Date::operator<(const Date& d)
{
//不>=就是小于,取反就行
if (!(*this >= d))
{
return true;
}
else
{
return false;
}
}
bool Date::operator<=(const Date& d)
{
//不>就是小于等于,取反就行
if (!(*this > d))
{
return true;
}
else
{
return false;
}
}
bool Date::operator!=(const Date& d)
{
//不==就是!=,取反就行
if (!(*this == d))
{
return true;
}
else
{
return false;
}
}
实现日期对象减日期对象的重载运算符
int Date::operator-(const Date& d)
{
//假设this最大,d最小
Date max = *this;
Date min = d;
int flag = 1;
//如果this小,d大就修正
if (*this < d)
{
max = d;
min = *this;
//符号位
flag = -1;
}
//统计天数
int n = 0;
while (min != max)
{
min++;
n++;
}
//符号处理
return flag * n;
}
const成员
const修饰的是成员函数的this
被const修饰的成员函数
被const修饰的成员函数是长这样的,如果不需要改变对象的值建议加const,防止误操
bool Date::operator==(const Date& d) const;
//在成员函数后面加const,const修饰的是this,
//实际的this已经被修改为:const Date *this,
//这是一个常量指针,表示的是指针所指向的内容不允许被修改
const与运算符重载结合使用
//返回一个可读可写的this对象
Date* Date::operator&()
{
return this;
}
//返回一个只读的this对象
const Date* Date::operator&()const
{
return this;
}
问题:
- const对象可以调用非const成员函数吗?
不能,这里会出现权限放大的问题,const修饰的对象是只读的并不能通过this来修改
- 非const对象可以调用const成员函数吗?
可以,权限的缩小,非const对象是可读可写的,即使传递给this,但this也被const修饰了不能改变对象的值
- const成员函数内可以调用其它的非const成员函数吗?
不能,权限放大,this已经被修饰为const了,将this传递给非const成员函数又是涉及一个权限放大的问题
- 非const成员函数内可以调用其它的const成员函数吗?
可以,权限的缩小,this是可读可写,即使使用this调用其他成员函数,也并不会有问题
Date *this ==》 const Date *this //权限缩小
总结:
1、const修饰的是this
2、权限的放大可以,但是权限的缩小不行
3、被const修饰的成员函数不能修改对象的值
友元
友元分为:友元函数和友元类提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
比如我们现在要解决这样的一个问题,重载一个operator<<函数打印对象的值,是写在类外面好,还是类里面好?先看写在类里面的
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1);
ostream& operator<<(ostream &out);
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 隐藏的this
//ostream& Date::operator<<(Date *this ,ostream& out)
ostream& Date::operator<<(ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
int main()
{
Date d1;
cout <<d1;
//cout 去调用重载后的operator<<会将cout对象的地址传递给this,
//而out实际接受的地址却是Date类型对象的地址,导致类型不匹配
return 0;
}
错误1、
原因参数传反,类型不匹配
cout <<d1;
//cout 去调用重载后的operator<<会将cout对象的地址传递给this,
//而out实际接受的地址却是Date类型对象的地址
报错信息:
解决办法:参数反过来传,既然this始终都是占第一个位置,那么就先传d1对象给this,再把cout对象给out
d1 <<cout; //虽然解决了问题,但是看起来会比较别扭
写成全局的?
void Date::operator<<(ostream& out, Date &d1)
{
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
写成全局的就不能再访问私有成员了,即使写成public,那样也会破坏类的封装性,不推荐
试试使用友元的效果
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1);
//把operator<<作为Date类的友元函数
friend ostream& operator<<(ostream& out, Date& d);
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//返回ostream对象是为了能够连续输出,出了作用域对象还在就可以返回引用
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{
Date d1;
cout << d1;//ok
return 0;
}
ostream& operator<<(ostream& out, Date& d)
从这行代码来看声明和定义即使分离开好像不需要再指定类域了,
如果加上后反而还会报错,因为operator<<函数只是属于这个类的友元并不是属于这个类,在类域中必然是找不到的,这是需要注意的地方。
同样的重载operator>>运算符也是一样的
//声明
friend istream& operator>>(istream& out, Date& d);
//定义
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
友元补充:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制, 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
- 友元不太推荐使用,会破坏类的封装性,没有合适的方法再使用
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、友元关系是单向的,不具有交换性。
比如这里Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour, int minute, int second)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _t(10,20,30)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
构造函数的补充
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
初始化列表
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值,所以在初始化的时候采用初始化列表初始化。
class A
{
public:
A(int x)
{
_x = x;
cout << "A() " << endl;
}
private:
int _x;
};
class Date
{
public:
Date(int year, int month, int day)
//初始化列表初始化
: _year(year)
, _month(month)
, _day(day)
, _n(0) //const成员必须采用初始化列表初始化
, _ra(_year) //引用必须使用初始化列表初始化
, _obj(10)
//初始化列表初始化这个对象会自动调用它的构造函数
{
//赋值
_day = day;
}
private:
int _year;
int _month;
int _day;
const int _n;
int& _ra;
A _obj;
};
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
1、引用成员变量
2、const成员变量
3、自定义类型成员(该类没有默认构造函数),通过指定构造函数调用初始化
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。初始化列表的方式效率会更高
做一做
请问这道题的输出结果是什么
class A
{
public:
A(int a)
:_a1(a) // 1,正常
,_a2(_a1) // 随机值,这里会用_a1去初始化_a2,但是在此之前a1是随机值
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1; }
int main() {
A aa(1);
aa.Print();
}
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,打印结果:1、随机值
注意:
初始化列表的顺序要和成员声明时的次序保持一致
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用, explicit关键字的作用就是防止单个参数的构造函数做隐士类型转换的操作
单参数的构造函数会支持隐士类型转换
class A
{
public:
explicit A(int a)//加上explicit不做隐士转换,之后的临时对象也不会产生了
:_a(a)
{
}
private:
int _a;
};
int main()
{
A a = 2;
//隐士类型转换,调用带单参数的构造函数将2作为实参传递给A(2),
//产生临时对象tmp,最后通过拷贝构造函数将临时对象tmp拷贝构造到a这个对象,
//现代编译器优化后直接调的构造
return 0;
}
匿名对象
调用构造函数产生的对象,不为这个对象取名字,那么这个对象就是匿名对象,匿名对象的生命周期只在当前这一行
A(10) //调用A(int)构造函数产生的匿名对象
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化,静态成员变量属于整个类,他是属于每一个对象,static成员不受访问限定符限制。
面试题:实现一个类,计算中程序中创建出了多少个类对象。
class A
{
public:
//构造
A()
{
++_scount;
}
//拷贝构造
A(const A& t)
{
++_scount;
}
static int GetACount()
{
return _scount;
}
private:
//_scount存在静态区,属于整个类,也属于类的所有对象
static int _scount;
};
//静态成员的定义初始化,不受访问限定符限制
int A::_count = 0;
void TestA()
{
cout<<A::GetACount()<<endl;//0
A a1, a2;
A a3(a1);
cout<<A::GetACount()<<endl; //3
}
总结:
- 静态成员为所有类对象所共享,不属于某个具体的对象,他是放在静态区的
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
- 静态成员不受访问限定符限制
问题:
7. 静态成员函数可以调用非静态成员函数吗?
class A
{
void f()
{
//代码
}
//静态成员函数去调用非静态的成员函数
static void func()
{
f();
}
};
不行,访问非静态的成员函数需要this,static静态成员函数没有隐藏的this指针
- 非静态成员函数可以调用类的静态成员函数吗?
class A
{
static void f()
{
//代码
}
//非静态成员函数去调用静态的成员函数
void func()
{
f();//突破类域就能访问到静态成员函数
}
};
可以,类中可以随便访问没有限制
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系
class A {
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1;
int main()
{
A::B b;
//内部类可以通过外部类的对象,
//这里是通过传递匿名对象给类B的成员函数 void foo(const A& a)
//有了类A的实例就能访问类A受保护的成员
b.foo(A());
return 0;
}