C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象和对象之间的交互关系,将一件事情拆分成不同的对象,靠对象之间的交互完成。
以外卖系统举例:面向过程就是:下单、接单、送餐的三个过程。面向对象则是:客户、商家、骑手之间的交互关系。
文章目录
- 一、类
- 1.1.类的定义
- 1.2.类的访问限定符及封装
- 1.2.1.访问限定符
- 1.2.2.封装
- 1.3.类的作用域
- 1.4.类的实例化
- 1.4.1.类对象的大小
- 1.4.2.给类对象赋值
- 1.5.隐含的this指针
- 1.6.定义匿名对象
- 二、类的六个默认成员函数
- 2.1.构造函数
- 2.2.析构函数
- 2.3.拷贝构造函数
- 2.4.赋值运算符重载
- 2.4.1.运算符重载
- 2.4.2.赋值运算符重载
- 三、使用运算符重载来实现日期类
- 四、const成员函数
- 4.1.取地址及const取地址操作符重载
- 五、输入和输出运算符重载
- 六、友元
- 七、初始化列表
- 八、explicit关键字
- 九、static成员
- 十、内部类
一、类
在C语言中,结构体是多种数据的集合,但是结构体中只能定义变量,不能定义函数。
在C++中引入了类的概念,它不仅可以定义变量,还可以定义函数。类中的变量称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
因此,类有两部分构成:1.成员变量(属性)2.成员函数(行为)
1.1.类的定义
类定义的基本语法为:
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类的成员函数有两种定义方式:
- 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 声明放在.h文件中,类的定义放在.cpp文件中。由于声明和定义不在同一个类域({}里面就是类域),因此在定义的时候要在成员函数的函数名前面加上类名和作用域解析符
::
。为了代码的可读性和可维护性一般采用这种方式。
1.2.类的访问限定符及封装
1.2.1.访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
为了兼容C语言,C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类,和class定义类是一样的。区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
1.2.2.封装
面向对象的四大特性:抽象、封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
即将数据和方法都放到类里面,希望类外可以调用的数据和方法定义成公有,不希望类外调用则可以定义成私有。所以 封装本质上是一种管理。 封装相比于不封装更加严格、更加规范,更不自由,可以降低代码出错的可能性。
1.3.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中(也就是大括号{}),整个类是一个整体。在类体外定义成员,需要使用 ::
作用域解析符指明成员属于哪个类域。
1.4.类的实例化
和结构体类似,用类创建一个对象,称为类的实例化。
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
- 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什
么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
1.4.1.类对象的大小
类对象的大小计算和C语言中结构体的大小计算类似,同样遵循内存对齐的规则,但类比结构体多了成员函数。而所有类对象都会调用同一份成员函数,如果每个对象都保存一份成员函数,则会造成很大的空间浪费。 因此对象的内存中只保存成员变量,成员函数存放在公共的代码段中。
在计算对象的大小时,只需计算成员变量的大小(遵循内存对齐的规则)即可。
另外,空类(没有成员变量的类)比较特殊,编译器给空类一个字节来占位,表示对象存在过。
1.4.2.给类对象赋值
利用成员函数可以给类对象赋值:
另外,类的成员变量的名称要和成员函数的形参名称进行区分,否则由于就进匹配原则,会找不到类的成员变量,要解决这个问题就要加上作用域。
1.5.隐含的this指针
从上面的内容可以知道,类的成员函数存储在公共代码区,因此类的所有对象调用的成员函数都是同一个函数,那么问题来了,编译器是如何知道是d1调用的该函数还是d2调用的该函数呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
因此,对于上面的函数调用d1.Init(2001, 1, 1);
,编译器会将其处理成d1.Init(&d1,2001, 1, 1);
。
类的成员函数也会被编译器处理成这个样子:
void Init(Date* this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
通过this指针,就可以区分是哪个对象调用的成员函数了。
this指针有以下特性:
- this指针是隐含的,是编译器编译时临时加的,我们不能在调用和定义函数时显示地加入this指针。
- 可以在成员函数中使用this指针。
- this指针是一个形参,所以this指针存储在栈中。不同的编译器不同,VS使用exc寄存器存储传参的。
- this是一个关键字,所以在定义变量时不能定义成this。
- this指针可以为空,但不能对空指针解引用。
1.6.定义匿名对象
定义的时候不加对象名即可定义一个匿名对象,但是这个匿名对象的生命周期只有定义的那一行。
使用场景:定义一个对象要用,但是只在定义的那一行使用,在其他地方不用。
二、类的六个默认成员函数
类里面成员函数我们什么都不写的时候,编译器会自动生成6个默认(缺省)成员函数。
2.1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数主要用来完成对象的初始化,相当于替代上面的初始化函数Init
,防止我们没有初始化就使用成员变量,因为我们可能会忘记调用Init
函数,而构造函数在对象创建时就自动调用。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载和缺省参数,也就意味着可以有多个构造函数。
class Date
{
public:
// 1.无参构造函数
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 2.带参构造函数
Date(int year, int month=10, int day=10)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}
int main()
{
TestDate();
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数(不用参数就可以调用的构造函数)。
- C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如:int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认构造函数初始化,但对内置类型并不会处理。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
_t.Print();
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
d.Print();
return 0;
}
缺省值: C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。如果初始化时没有传参,则会使用我们给的缺省值来初始化。
2.2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作,比如完成栈的销毁与释放。
析构函数是特殊的成员函数。
其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值,因此不能重载。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统自动调用析构函数。
- 有多个对象时,由于对象时定义在函数中,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出,因此后构造的对象先析构。
- 和构造函数相同,内置类型成员不会处理,自定义类型成员会去调用它的析构函数。
2.3.拷贝构造函数
在一个对象创建时可以用一个已经存在的对象初始化这个新的对象。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在一个对象创建时用这个引用初始化这个新的对象。
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用(因为值传递的形参也要调用拷贝构造)。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。因此,如果需要深拷贝,则需要我们自己写。
- 如果没有实现拷贝构造函数,则编译器对内置类型会调用自己的拷贝构造函数,对自定义类型会调该类型的拷贝构造完成拷贝。
深拷贝和浅拷贝:
编译器生成的默认拷贝构造函数可以完成字节序的值拷贝,这种方式叫做浅拷贝。
浅拷贝对日期等没有问题,但是对于栈、字符串常量等,浅拷贝可能会出现问题。
深拷贝则是会开辟一份新的空间,需要我们自己实现。
拷贝构造函数调用时有两种写法:
2.4.赋值运算符重载
2.4.1.运算符重载
内置类型在语言层面支持运算符,但是自定义类型默认不支持。所以为了自定义类型也能使用+-==
这些运算符,并且增强程序的可读性,就要用到运算符重载。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
语法:返回值类型 operator操作符(参数列表)
以判断两个日期类是否相等为例:
由于日期类中的成员变量是私有的,将运算符重载为全局函数则不能访问其成员变量,所以可以重载为成员函数。
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
运算符重载的一些注意事项:
- 运算符重载和函数重载都用了“重载”这个词,但是两者没有关联,函数重载是支持定义同名函数,运算符重载是为了让自定义类型可以像内置类型一样去使用运算符。
- 不能通过重载来创建新的运算符:比如operator@,C语言和C++中没有@这个运算符,所以不能重载@
- 重载操作符必须有一个自定义类型或者枚举类型的操作数,也就是参数必须有一个自定义类型或者枚举类型
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的,因为操作符有一个默认的形参this,限定为第一个形参
.* 、 :: 、 sizeof 、 ?: 、.
注意以上5个运算符不能重载。
2.4.2.赋值运算符重载
赋值运算符重载也是拷贝行为,但是不一样的是,拷贝构造函数是创建一个对象时,拿同类对象初始化的拷贝。赋值拷贝是两个对象都已经存在且初始化过了,将一个对象的值拷贝给另一个对象。
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符需要注意四点:
- 参数传引用来提高效率,加const,防止对d进行修改。
- 返回值要返回一个引用 *this,否则将无法连续赋值。
- 检测是否自己给自己赋值。
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,和默认拷贝构造函数特性是一样的,只能完成浅拷贝。
三、使用运算符重载来实现日期类
通过重载运算符+-<>
等来实现日期类的比较,其中应该注意代码的复用,部分函数可以通过调用已经写好的函数来实现:
#include<iostream>
#include<assert.h>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0);
void Print();
//析构、拷贝构造、赋值重载可以不写,因为默认生成的就够用
//天数相加
Date& operator+=(int day);
Date operator+(int day);
//日期相减
Date& operator-=(int day);
Date operator-(int day);
//++d->d.operator++(&d)
Date& operator++();
//d++->d.operator++(&d,0)
//int参数不需要给实参,它的作用是为了跟前置++构成函数重载b
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);
private:
int _year;
int _month;
int _day;
};
//获取某年某月的天数,因为要多次调用,可以写成内联函数
inline int GetMonthDay(int year, int month)
{
static int dayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day= dayArray[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>0&&day<=GetMonthDay(year,month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期";
cout << year << "-" << month << "-" << day << endl;
}
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date& Date :: operator+=(int day)
{
if (day < 0)
{
*this -= (-day);
}
else
{
//天满了,减去当月的天数,月+1
//月满了,年+1,月置成1月
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_day, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
}
return *this;
}
Date Date :: operator+(int day)
{
//通过调用+=函数来实现
Date ret(*this);
//月满了,年+1,月置成1月
ret += day;
return ret;
}
Date& Date :: operator-=(int day)
{
if (day < 0)
{
*this += (-day);
}
else
{
//天不够,加上当月的天数,月01
//月不够,年-1,月置成12月
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_day, _month);
}
}
return *this;
}
Date Date:: operator-(int day)
{
//通过调用-=来实现
Date ret(*this);
ret -= day;
return ret;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//比较大小只需要实现>和==即可,其他比较操作符都可以用这两个操作符实现,提高代码复用率
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_day > d._day)
{
return true;
}
}
return false;
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
bool Date::operator>=(const Date& d)
{
return *this > d || *this == d;
}
bool Date::operator<=(const Date& d)
{
return !(*this > d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
int Date::operator-(const Date& d)
{
//只需要让小的日期一直加,加到和大的日期相等即可算出差了多少天
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
n++;
}
return n * flag;
}
四、const成员函数
有些函数尽可能加上const防止*this被修改:
所以如果成员函数中不需要改变成员变量,建议加上const
const的对象和函数的几个问题:
-
const对象不可以调用非const成员函数,因为是权限的放大。
-
非const对象可以调用const成员函数,因为这是权限的缩小。
-
const成员函数内不可以调用其它的非const成员函数,因为权限放大。
-
非const成员函数内可以调用其它的const成员函数,因为权限缩小。
4.1.取地址及const取地址操作符重载
取地址函数和const取地址也是六个默认成员函数中的两个,所以可以不用写重载,编译器默认生成的就够用了。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
五、输入和输出运算符重载
一个类的输入输出可以调用它们自己的输入和输出的成员函数,也可以重载>>和<<两个运算符来起到输出的作用。
cout 是 ostream 类的对象。cin 是 istream 类的对象。这俩类也有其他它们都在头文件 < iostream > 中声明。
另外>>和<<在输入输出时可以自动识别内置类型,因为它们在库里面已经被写好了各种类型的函数重载了:
不同于内置类型,如果将自定义类型的>><<重载成成员函数的话,如下面这样:
则<<的左操作数必须是类的对象,右操作数才是cout,这样代码的可读性会很差,因此可以将<<>>重载为全局函数,但是它们需要访问类的私有成员变量,因此在类定义中将它们声明为友元。
这样<<>>的左右操作数就和原来一样了。
参数in和out只能是引用,因为它istream和ostream的复制构造函数是私有的,没有办法生成对应的参数对象。它们的返回值都是istream和ostream的引用,因为只有这样才能连续输入和输出。
六、友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
其语法为在类中用friend关键字声明函数或者其他类。
在<<和>>的重载中就用到了友元函数,友元函数有以下注意事项:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
同理也有友元类:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元类有以下注意事项:
- 友元关系是单向的,不具有交换性。
比如A类和B类,在A类中声明B类为其友元类,那么可以在B类中直接访问A类的私有成员变量,但想在A类中访问B类中私有的成员变量则不行。 - 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
七、初始化列表
构造函数的初始化有两种,上面的一种为构造函数体内初始化,还有一种叫做初始化列表初始化:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
以日期类初始化为例:
Date(int year=0, int month=0, int day=0)
: _year(year)
, _month(month)
, _day(day)
{}
函数体内初始化和初始化列表初始化可以混用。
初始化列表的注意事项:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量、const成员变量、自定义类型成员(该类没有默认构造函数)
一个对象的单个成员变量在初始化列表初始化是在其定义的阶段初始化,而在函数体内初始化就相当于定义完后再进行初始化赋值,类似于下面这样:
int a=1;//初始化列表
int a;
a=1;//函数体内初始化
这也就解释了为什么上面三种成员为什么 必须要用函数初始化列表初始化,因为它们必须在定义的时候就初始化。
- 自定义类型尽量使用初始化列表初始化,对于自定义类型成员变量,如果不使用初始化列表,只能现在函数体内初始化一个临时对象,然后将临时对象赋值给要初始化的对象。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,所以建议类中成员变量声明的顺序和初始化列表出现的顺序保持一致。
先给初始化_a2,然后初始化_a1
八、explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
实际编译器背后会用2019和2010构造一个临时对象,最后用无名对象给d1和d2对象进行赋值:
Date tmp(2019),d1=tmp;
Date tmp(2010),Date d2(tmp)
如果不想有这种优化,可以用explicit关键字取消优化:
九、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。
static修饰的变量和函数存在静态区中,并不存在由类定义的对象中。
实现一个类,计算中程序中创建出了多少个类对象:
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
static成员的特性:
- 静态成员为所有类对象所共享,不属于某个具体的对象,它是放在静态区的,可以突破类域访问
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员,只能访问静态成员
- 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
- 非静态成员函数可以调用类的静态成员函数
十、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的
类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)求的大小和内部类成员没有任何关系