文章目录
一、面向过程与面向对象的区别
我们知道,C语言是面向过程的语言,而C++是面向对象的语言。
面向过程的理解是关注的是过程,关注这个问题是怎么实现的,实现的具体过程是什么,分析出求解问题的步骤。
而面向对象的理解是我们关注的是对象,并不关注问题解决的具体过程,我们只需要将一件事情拆分成不同的对象,通过对象之间的交互来完成这一件事情。
举个具体的例子,我们在坐高铁的时候,不需要知道高铁的购票系统、进站系统、分配系统是怎么具体实现的,我们只需要利用系统提供给我们的接口去使用这个系统,不关注实现的过程关注的是对象,同样能够完成我们要做的事情。这就是面向过程和面向对象的区别。
二、类的定义
类是面向对象语言中用来实现信息封装的基础。一般我们将我们需要的功能实现封装成一个类,这一个类实例化出来的称作对象,一个类可以实例化出很多个对象,虽然是同一个类实例化出的对象,但每个对象都是相互独立互不影响的。
在C语言中,我们的自定义类型struct定义结构体,结构体中只能定义变量。但在C++中将struct升级成了类,不仅可以定义变量,也可以定义函数。
struct student
{
//定义函数
void print()
{
cout << _name << "-" << _age << endl;
}
//定义变量
char _name[21];
int _age;
};
我们通常使用class来定义类,定义的方法和struct相同。
class Name
{
//类体:由成员变量和成员函数组成
};
类中的元素称为类的成员,由成员变量和成员函数组成。
类的定义方式有两种:
- 声明和定义都在类体中(成员函数在类中定义,编译器可能会将其当成内联函数处理)
- 声明放在.h文件中,定义放在.cpp文件中(我们一般更常使用这一种定义方式)
三、封装
面向对象的三大特性是 封装、继承、多态。在类和对象中,我们需要明白封装的含义。
封装是将数据和操作数据的方法(也就是函数)结合起来,并且隐藏数据以及操作数据的方法,也就是说别人无法知道操作数据的具体方法,只能通过对外公开的接口来和对象进行交互,从而实现各种操作。这就是封装的含义。我们将类的数据和操作数据的方法都封装到了一起,不想让别人获取的数据我们可以保护起来,只开放一些共有的成员函数对成员变量进行合理的访问(外界无法改变这种访问方法,外界只有使用的权力),所以封装本质上是一种管理,是为了让外界必须按要求来访问我们的成员变量,不能随心所欲,否则容易出现错误。
举个例子:比如我们想要实现一个栈,栈的特性是先进先出,我们如何规定这种先进先出的规则呢?万一有人不按规则来访问,想要对栈进行随机访问那就出现错误了。我们为了避免这种错误发生,我们需要将栈的成员变量保护起来,并且提供符合规则的访问函数,外界只能通过我们函数的接口来使用栈,这样就能保证不会出现错误。
C++实现了这种封装的方式,就是用类将对象的数据和操作数据的方法结合在一起,通过设置访问权限从而有选择地将其接口提供给外部用户使用。
C++提供访问限定符来确定访问权限:
- public:成员可以在类外直接访问
- private:成员在类外不能直接访问
- protected:成员在类外不能直接访问
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
- class的默认访问权限为private,struct的默认访问权限为public(因为struct需要兼容C语言)
访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
四、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域钟,所以在类体外定义的成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
class Func
{
public:
int Add(int left, int right);
};
//Add声明在类体内,定义在类体外,需要在定义时指明其属于的类域
int Func::Add(int left, int right)
{
return left + right;
}
五、this指针
我们先来看下面的代码示例:
class Date
{
public:
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;
Date d2;
d1.SetDate(2022, 10, 1);
d2.SetDate(2021, 5, 1);
return 0;
}
这里会有一个疑问:在Date类中的成员函数SetDate并没有区分不同的对象,那当s1对象在调用该函数的时候,它是怎么知道设置的是s1对象而不是s2对象呢?
在这里C++引入了this指针来解决这个问题,C++编译器给每个 “非静态成员函数” 增加了一个隐藏的指针参数,让这个指针指向当前对象(也就是函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过this指针去访问,只不过所有的操作对用户都是透明的,用户不需要自己传递this指针,编译器会自动生成。
也就是说上述示例代码,可以理解为如下形式:
class Date
{
public:
void SetDate(Date* this,int year,int month,int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
注意:this指针不能显式去定义,我们只需要知道有这个指针的存在,并不需要我们自己去提供,编译器会自动生成,且我们自己提供编译是不通过的。
this指针的特性:
- this指针的类型为:类类型* const this
- this指针只能在成员函数的内部使用
- this指针本质上是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参(注意是成员函数形参里的第一个),一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。
- this指针存在栈上面,但有些编译器会使用寄存器优化,因为有些函数需要多次重复使用this指针,寄存器访问较快
六、运算符重载
内置类型,可以直接用各种运算符,但自定义类型,不能直接用各种运算符。为了让自定义类型可以使用各种运算符,制定了运算符重载的规则
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:关键字operator 后面接需要重载的运算符符号
运算符重载注意事项:
-
不能通过连接其他符号来创建新的操作符
-
重载操作符必须有一个类类型或者枚举类型的操作数
-
用于内置类型的操作符,其含义不能改变
-
作为类成员的重载函数时,其形参看起来比操作数数目少一个,因为成员函数有一个默认的形参this,限定为第一个形参
-
以下5个运算符不能重载:::(域作用限定符) 、 sizeof 、 ?:(三目运算符) 、 .(对象的点) 、 .*
-
如果类里面有运算符重载,同时全局也有运算符重载,那么编译器会优先到类里面去寻找运算符重载
class Date
{
public:
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
七、const成员
我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。
我们看看下面的代码示例:
Func函数的形参给的是const修饰的变量d,当d去调用Date类里的print函数的时候,会编译不通过,原因是const修饰的变量d去调用没有const修饰的成员函数时属于权限放大,权限只能缩小或者相同不能够放大,因此调用不成功。
void Func(const Date& d)
{
d.print();//d.print(&d) -> const Date*
}
class Date
{
void print ()
{
cout<<endl;
}
};
我们只有在类的成员函数里加上const,按理来说应该是Date* const this前面加上const,即const Date* const this,但是由于this我们不能够显式定义,因此C++规定可以直接在函数名和括号后加const
void Func(const Date& d)
{
d.print();//d.print(&d) -> const Date*
}
class Date
{
void print ()const
{
cout<<endl;
}
};
注意:传值返回时返回的是一份临时的拷贝,这个拷贝具有常性,是const的
- const对象可以调用非const成员函数吗?——不能,权限放大
- 非const对象可以调用const成员函数吗?——可以,权限缩小
- const成员函数内可以调用其它的非const成员函数吗?——不能,权限放大
- 非const成员函数内可以调用其它的const成员函数吗?——可以,权限缩小
八、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类,但空类中并不是什么都没有,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数:
- 构造函数:主要完成初始化工作
- 析构函数:主要完成资源清理工作
- 拷贝构造:使用同类对象初始化创建对象
- 赋值重载:主要是把一个对象赋值给另一个对象
- 取地址重载:主要是普通对象和const对象取地址
下面我们详细看看类的6个默认成员函数:
1.构造函数
构造函数是一个特殊的成员函数,函数名字必须与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
class Date
{
public:
//无参构造函数
Date()
{
}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用无参构造函数
Date d1;
//调用带参构造函数
Date d2(2022, 8, 23);
return 0;
}
构造函数的特性:
- 构造函数的函数名必须与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 构造函数虽然名字叫构造,但其主要任务并不是开空间创建对象,而是初始化对象。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了构造函数则编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构造函数都可以认为是默认成员函数)
- 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理,会去调用该自定义类型的构造函数
class Date
{
public:
//无参构造函数
Date()
{}
//带参构造函数
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
//全缺省构造函数
Date(int year=0,int month=0,int day=0)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
int _day;
};
2.析构函数
析构函数的功能与构造函数相反,析构函数不是完成对象的销毁,因为局部对象的销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
class Stack
{
public:
Stack(int capacity=10)
{
int* a=(int*)malloc (sizeof (int)*capacity);
if (a==nullptr)
{
cout<<"malloc fail"<<endl;
exit(-1);
}
_a=a;
_top=0;
_capacity=capacity;
}
//析构函数
~Stack()
{
free(_a);
_a=nullptr;
_top=0;
_capacity=0;
}
private:
int* _a;
int _top;
int _capacity;
}
析构函数的特性:
- 析构函数的函数名时在类名前加上字符~
- 无参数无返回值
- 一个类有且只有一个析构函数,如果没有显式定义,系统会自动生成默认的析构函数
- 对象的生命周期结束时,C++编译系统自动调用析构函数
- 编译器默认生成的析构函数,对于内置类型不做处理,对于自定义类型会去调用它的析构函数
- 栈里面定义的对象,由于栈是先进后出,所以后定义的对象先析构
3.拷贝构造函数
拷贝构造函数在创建对象时,可以实现创建一个与另一个已经存在的对象一模一样的新对象。拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class Date
{
public:
Date(int year=0,int month=0,int day=0)
{
_year=year;
_month=month;
_day=day;
}
//拷贝构造函数
Date(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
private:
int _year;
int _month;
int _day;
};
int main ()
{
Date d1;
//调用拷贝构造
Date d2(d1);
return 0;
}
拷贝构造函数的特征:
- 拷贝构造函数其实是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须使用引用传参(如果使用传值传参会导致无穷递归调用拷贝构造函数从而导致程序崩溃)
- 如果没有显式定义,系统会生成默认的拷贝构造函数,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝被称为浅拷贝,或值拷贝
4.赋值运算符重载
赋值运算符重载与拷贝构造不同的地方在于,拷贝构造是一个存在的对象去初始化另一个要创建的对象,而赋值运算符重载,是两个已经存在的对象之间赋值。
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
//防止自己给自己赋值
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 22);
Date d2(2022, 3, 30);
d2 = d1;
return 0;
}
赋值运算符重载的特性:
- 形参最好采用引用,返回类型最好也是引用返回,能够减少拷贝
- 注意检查是否自己给自己赋值
- 一个类如果没有显式定义赋值运算符重载,编译器会默认生成一个,但完成的是浅拷贝
5.取地址及const取地址运算符重载
这两个运算符一般不需要我们去重载,使用编译器生成的默认成员函数就够用了。
Date* operator& ()
{
return this;
}
//const取地址运算符重载
const Date* operator& () const
{
return this;
}
九、构造函数的初始化列表
初始化列表,是构造函数进行初始化的另一种形式,初始化列表可以认为就是对象成员变量定义的地方,在主函数里Date d1(2022,7,31)是对象的定义
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
Date (int year=1,int month=1,int day=1)
:_year(year)
,_month(month)
,_day(day)
{}
注意事项:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 有三类成员必须在初始化列表里初始化:引用成员变量,const成员变量,自定义类型成员(该类没有默认构造函数)
- 尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型的成员变量,一定会先使用初始化列表初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
十、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称为静态成员变量;用static修饰的成员函数,称为静态成员函数。
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
private:
static int _count1;
static int _count2;
}
//声明在类里面,定义要在类外面定义
int A::_count1=0;
int A::_count2=0;
static成员的特性:
- 静态成员变量属于整个类,属于类的所有对象,不是存在某个对象类,而是任何的对象都可以访问到他们,所以sizeof计算类的大小不算静态成员变量
- 静态成员变量必须在类外定义,声明在类内部,定义时不添加static关键字
- 成员函数也可以是静态的,静态的成员函数没有this指针,静态成员函数不能访问任何非静态成员(因为没有this指针,不知道你访问的是哪一个对象的成员)
- 静态成员和类普通成员一样,也有public、protected、private三种访问级别,也可以具有返回值
十一、友元
友元分为友元函数和友元类
友元提供了一种突破封装的方式,有时候提供了便利。但是友元会增加耦合度,破坏封装性,所以友元不宜多用。
友元类:在类的声明前加上friend就可以使用了
//友元类
class Time
{
friend class Date;
private:
int _hour;
int _minute;
int _second;
};
class Date
{
//在Date类里面就能访问Time里面私有的成员变量了
}
友元类的说明:
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性
- 友元关系不能传递
友元函数:在函数声明前面加上friend就可以使用了
class Date
{
public:
friend void Print (const Date& d)
{
}
friend void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
//使用友元函数可以让类外面的函数访问类里面的私有的成员变量
void print(const Date& d)
{
}
友元函数的说明:
- 友元函数可以访问类的私有和保护成员,但它不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数调用的原理相同
十二、C++11的成员初始化新玩法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,只是给声明的成员变量缺省值
class Date
{
public:
private:
//非静态成员变量,可以在成员声明时给缺省值
int _year=1;
int _month=1;
int _day=1;
};