都说C++的编程思想是面向对象的
那么什么是面向对象??
面向对象程序设计是一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的实例,将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重要性、灵活性和扩展性。
面向对象是将数据和操作数据的函数紧密结合。面向对象的三大特性
1.封装:将现实世界中的数据和对数据进行操作的动作捆绑在一起形成类,然后再通过类定义对象,很好的实现了对现实世界事物的抽象和描述2.继承:可以在旧类型的基础上快速派生得到新的类型,很好的实现了设计和代码复用
3.多态:保证了在继承的同时,还有机会对已有行为进行重新定义,满足了不断出现的新需求的需要
正因为面向对象思想的三大特性,使得面向对象思想在程序设计中有着不可替代的优势
1.容易设计和实现
2.复用设计和代码,开发效率和系统质量都得到了提高
3.容易扩展
C++中有三种访问限定符
public–共有
protected–保护
- private–私有
1.public成员可从类外部直接访问,private/protected成员不能从类外部直接访问。
2.每个限定符在类体中可使用多次,它的作用域是从该限定符出现开始到下一个限定符之前或类体结束前。
3.类体中如果没有定义限定符,则默认为私有的。
4.类的访问限定符体现了面向对象的封装性。
类的大小如何计算?
类的大小是有成员变量决定的,因为不同对象成员变量不同,但成员函数相同所以成员函数存放在常量区(代码段)
类的大小与成员函数和静态成员无关
- 虚函数会影响类的大小,因为虚函数表指针的影响
- 虚继承会影响类的大小,因为虚基表指针的影响
- 空类的大小是1个字节,为了占位表示这个类型的对象存在过,不同的对象不能具有相同的地址,这是由于new需要分配不同的内存地址,不能分配内存大小为0的空间,避免除以sizeof(T)时得到除以0错误
- 类的大小的计算也要遵循结构体的对齐原则
结构体内存对齐的原则:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
//对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
gcc中的默认值为4
3.结构体总大小为最大对齐数的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
那么为什么要内存对齐呢??
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如有些平台每次读都是从偶地址开始,一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多
性能原因:经过内存对齐后,CPU的内存访问速度大大提升。用空间换时间的思想
接下来介绍一下类的4个默认成员函数
构造函数(原子的)
- 成员变量为私有的,要对他们进行初始化,必须用一个公有成员函数来进行。同时这个函数应该有且仅在定义对象时自动执行一次
特征:
1.函数名与类名相同
2.无返回值
3.对象构造(对象实例化)时系统自动调用对应的构造函数
4.构造函数可以重载(函数名相同、参数不同)
5.构造函数可以在类中定义,也可以在类外定义
6.如果类定义中没有给出构造函数,则C++编译器自动生成一个缺省的构造函数,但只要我们定义了一个构造函数,系统就不会生成缺省的构造函数
7.无参的构造函数和全缺省的构造函数都认为是缺省构造函数,并且缺省的构造函数只能有一个
class Date
{
public:
Date()//无参构造函数
{}
Date(int year,int month,int day)//带参构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(int year = 2000,int month = 1,int day = 1)//缺省参数的构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(int year,int month = 1)//半缺省参数的构造函数
{
_year = year;
_month = month;
_day = 1;
}
private:
int _year;
int _month;
int _day;
};
深入探索构造函数
类的成员变量有两种初始化方式:
1.初始化列表
以一个冒号开始,接着一个逗号分隔数据列表,每个数据成员都放在一个括号中进行初始化。尽量使用初始化列表进行初始化,因为它更高效
2.构造函数体内进行赋值
#include <iostream>
class Time
{
public:
Time()
{
std::cout<<"Time()"<<std::endl;
_hour = 0;
_minute = 0;
_second = 0;
}
Time(const Time& t)
{
std::cout<<"Time(const Time& t)"<<std::endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
//非初始化列表
Date(int year,int month,int day,const Time& t)
{
std::cout<<"Date()--非初始化列表"<<std::endl;
_year = year;
_month = month;
_day = day;
_t = t;
}
//初始化列表
Date(int year,int month,int day,const Time& t)
:_year(year)
,_month(month)
,_day(day)
,_t(t)
{
std::cout<<"Date()--初始化列表"<<std::endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
void Test()
{
Time t1;
Date d1(2015,4,29,t1);
}
int main()
{
Test();
return 0;
}
- 看上述的两段代码许多同学会产生疑问,什么使用初始化列表进行初始化,比使用非初始化列表更高效呢??
初始化参数列表在对象初始化时对成员变量赋值一次
构造函数内直接赋值,对成员变量赋值两次,一次是对象构造时用默认值进行赋值,第二次是调用构造函数赋值
有一些成员变量必须放在初始化列表里:
1.常量成员变量(常量创建时必须初始化,不能赋值)
#include <iostream>
class Time
{
public:
Time()
{
std::cout<<"Time()"<<std::endl;
_hour = 0;
_minute = 0;
_second = 0;
}
Time(const Time& t)
{
std::cout<<"Time(const Time& t)"<<std::endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year,int month,int day,const Time& t)
{
std::cout<<"Date()--非初始化列表"<<std::endl;
_year = year;
_month = month;
_day = day;
_t = t;
}
private:
int _year;
int _month;
int _day;
const int _testConst;//测试const成员变量的初始化
Time _t;
};
void Test()
{
Time t1;
Date d1(2015,4,29,t1);
}
int main()
{
Test();
return 0;
}
2.引用类型成员变量(引用必须在定义的时候初始化,并且不能重新赋值)
#include <iostream>
class Time
{
public:
Time()
{
std::cout<<"Time()"<<std::endl;
_hour = 0;
_minute = 0;
_second = 0;
}
Time(const Time& t)
{
std::cout<<"Time(const Time& t)"<<std::endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year,int month,int day,const Time& t)
{
std::cout<<"Date()--非初始化列表"<<std::endl;
_year = year;
_month = month;
_day = day;
_t = t;
}
private:
int _year;
int _month;
int _day;
int& _testReference;//测试引用成员变量的初始化
Time _t;
};
void Test()
{
Time t1;
Date d1(2015,4,29,t1);
}
int main()
{
Test();
return 0;
}
3.没有缺省构造函数的自定义类型的成员变量(因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化)
#include <iostream>
class Time
{
public:
Time(const Time& t)
{
std::cout<<"Time(const Time& t)"<<std::endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year,int month,int day,const Time& t)
{
std::cout<<"Date()--非初始化列表"<<std::endl;
_year = year;
_month = month;
_day = day;
_t = t;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Test();
return 0;
}
注:成员变量按声明顺序依次初始化,而非初始化列表出现的顺序
#include <iostream>
class Date
{
public:
Date(int x)
:_day(x)
,_month(_day)
,_year(x)
{
std::cout<<"Date()"<<std::endl;
}
void Display()
{
std::cout<<"year:"<<_year<<std::endl;
std::cout<<"month:"<<_month<<std::endl;
std::cout<<"day:"<<_day<<std::endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(1);
d1.Display();
}
int main()
{
Test();
return 0;
}
先声明的是_year所以先初始化的是年,年是1,然后初始化的_month,但是_month是根据_day来初始化的,由于这时的_day还没有进行初始化,所以_month是一个随机值,最后初始化_day,是1
拷贝构造函数
- 创建对象时使用同类对象来进行初始化,这时所用的构造函数称为拷贝构造函数,拷贝构造函数是一种特殊的构造函数
特征:
1.拷贝构造函数其实是一个构造函数的重载
2.拷贝构造函数的参数必须使用引用传参,使用传值方式会引发无穷递归调用
3.若未显示定义,系统会默认缺省的拷贝构造函数。缺省的拷贝构造函数会依次拷贝类成员进行初始化
class Date
{
public:
Date()//无参构造函数
{}
//拷贝构造函数
Date(const Date& d)//在类的成员函数中可以直接访问同类对象的私有/保护成员
//C++的访问限定符是以类为单位的,也就是说在这个单位内的成员可以互相访问
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
析构函数
- 当一个对象的生命周期结束时,C++编译系统会自动调用一个成员函数,这个特殊的成员函数即析构函数
特征:
1.析构函数在类名加上字符~
2.析构函数无参数无返回值
3.一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省的析构函数
4.对象生命周期结束时,C++编译系统会自动调用析构函数
5.注意析构函数体内并不是删除对象,而是做一些清理工作
class Date
{
public:
Date()//无参构造函数
{}
//拷贝构造函数
Date(const Date& d)//在类的成员函数中可以直接访问同类对象的私有/保护成员
//C++的访问限定符是以类为单位的,也就是说在这个单位内的成员可以互相访问
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//析构函数
~Date()
{}
private:
int _year;
int _month;
int _day;
};
赋值运算符重载
- 对一个已经存在的对象进行拷贝赋值
- C++支持运算符重载,运算符重载的意义:为了增强程序的可读性
特征:
1.operator+合法的运算符 构成函数名(重载<运算符的函数名:operator<)
2.重载运算符以后,不能改变运算符的优先级/结合性/操作数个数
C++不能重载的5个运算符
(1).* (2):: (3)sizeof (4)?: (5).
class Date
{
public:
Date()//无参构造函数
{}
//拷贝构造函数
Date(const Date& d)//在类的成员函数中可以直接访问同类对象的私有/保护成员
//C++的访问限定符是以类为单位的,也就是说在这个单位内的成员可以互相访问
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//析构函数
~Date()
{}
//赋值操作符的重载
Date& operator=(const Date&d)
{
if(this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
在编写操作运算符重载代码时会出现一些问题:
1.为什么operator=赋值函数需要一个Date&的返回值,使用void做返回值可以吗?
(1)在函数返回时避免一次拷贝,提高了效率
(2)可以实现连续赋值(a=b=c)。如果不是返回引用而是返回值类型,那么执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对返回的内容进行一次拷贝,得到一个未命名的副本(匿名对象),然后将这个副本返回,而这个副本是右值,所以执行a=b后,得到的是一个右值,再执行=c就会出错。
(3)如果返回的是类对象本身,其所有过程如下:
A.释放对象原来的堆资源
B.重新申请堆空间
C.拷贝源的值到对象的堆空间的值
D.创建临时对象(调用临时对象拷构造函数),将临时对象返回
E.临时对象结束,调用临时对象析构函数,释放临时对象堆内存
如果赋值运算符返回的是类对象本身,那么一定要overload类的拷贝函数(为了进行深拷贝)
(4)如果赋值运算符返回的是对象的引用,其所有过程如下:
A.释放掉原来对象所占有的堆空间
B.申请一块新的堆内存
C.将源对象的堆内存的值copy给新的堆内存
D.返回源对象的引用
E.结束
如果赋值运算符返回的是对象引用,那么其不会调用类的拷贝构造函数
2.为什么赋值运算符返回的是对象引用就可以连续赋值?
当运算符重载返回的是对象时,会在连续赋值运算过程的返回途中,调用两次拷贝构造函数和析构函数。如果返回的是对象的引用这样就不会有多余的调用
3.为什么要有if(this != &d)这句判断?
避免自我赋值a=a这样的情况发生
4.为什么传入的是const Date& d?
(1)不希望在这个函数中对用来进行赋值的原版做任何修改
(2)加上const,对于const的和非const的实参,函数都能接受;如果不加const,就只能接受非const的实参
(3)用引用可以避免在函数调用时对实参的一次拷贝提高了效率