目录
类的定义
C语言是面向过程的,关注的是过程,而C++则是面向对象的。面向过程就需要关注整个流程的实现的步骤和方法,面向对象则更关注实现某个事物参与的对象有哪些。
有时候不希望对外提供实现的方法及对象的属性,只希望向用户提供对应功能的接口,用户直接使用,C语言是无法做到对数据的封装的,所以C++引入类这一概念来实现。
C语言的结构体只能定义变量,所以C语言的数据和数据的实现方法是分离的,这会导致我们既要传数据,又要传调用的结构体,很麻烦。C++的结构体既能定义变量,还能定义函数,这样就可以将数据及数据实现的方法进行有机结合。
类的定义
cpp
class name
{
//内体由变量及函数组成
}; //分号
class是定义类的关键字,name是类的名称。类的成员被称为内的属性和成员变量,类的函数称为类的实现方法或成员函数。
在C++中为了实现对数据进行封装,引入了访问访问限定符的概念。
++限定符分为三种:1)public(公有);2)private(私有);3)protected(保护);++
pubic修饰的类外可以直接访问,private和protected类外是不能直接访问的。
cpp
class test
{
public:
int add()
{
return _x + _y;
}
private:
int _x;
int _y;
};
限定符说明
1)限定符的作用域是从该限定符开始到下一个限定符,若没有下一个限定符,则直接到类结束;
2)class的访问权限中,public是公有,private和protected是私有;
3)struct中默认访问权限是public。class中默认访问权限是private;
补充
面向对象的三大特性:封装,继承,多态;
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,对外公开接口来和对象进行交互。封装的本质是更好的管理,让用户更方便的使用类。
类的实例化
用类模板定义对象的操作叫做类的实例化。
cpp
class test
{
public:
int add()
{
return _x + _y;
}
private:
int _x;
int _y;
};
int main()
{
test s1; //类模板的实例化,s1就是类对象
return 0;
}
定义出的类实际上是不会分配空间存储的,而类对象则需要空间进行存储;类就是图纸,而类对象就是依据图纸得到的产品。
类对象的大小
类是没有大小的,只有在类模板实例化后才有大小,类对象的大小是其成员变量经过内存对齐之后的大小。注意类函数是不加到类对象的大小,类函数是类中公有的,创建对象是不需要为其开辟空间,需要调用的时候直接去其地址处调用即可,就好比自己家和小区篮球场的关系,需要的时候直接去使用,自己不用再为其开辟空间。
注意:没有成员变量的类对象也需要1个字节的大小进行占位。
类中隐藏的this指针
this指针的引入
cpp
class test
{
public:
void Init(int x, int y)
{
_x = x;
_y = y;
}
private:
int _x;
int _y;
};
int main()
{
test t1;
test t2;
t1.Init(1, 2);
t2.Init(3, 4);
return 0;
}
++上述两个test的类对象,在调用Init的时候,编译器是怎么将其分开的,在调用的时候,编译器怎么知道对t1还是t2进行处理的??++
C++中引入了隐藏的this指针来解决这一问题。如上图所示,t1和t2都有其this指针分别是&t1和&t2,通过这种方式来区分是哪一个对象在调用类函数。
this指针的特性
1)this指针是不能显示调用的,但是在类函数内部是可以显示使用的;
cpp
class test
{
public:
void Init(int x, int y)
{
this->_x = x; //其本意就是,_x=x;
this->_y = y;
}
private:
int _x;
int _y;
};
2)this指针实际上是,成员函数的形参,当调用成员函数的时候,将对象地址作为实参传递给this形参。所以对象中是不储存this指针的。
3)this指针作为形参不需要用户自己传,编译器会自动传递。
类的默认成员函数

类为了解决每次实例化对象后都要调用构造,开辟空间,出作用域又要销毁等问题,类设置了6个默认成员函数,其可以由编译器自动调用。
构造函数
构造函数是为了解决忘记对类对象进行初始化的问题。构造函数是一种特殊的类成员函数;
特征
1)函数名与类名相同;
2)无返回值;
3)对象实例化时编译器会自动调用对应的构造函数;
4)构造函数可重载。
5)如果类中没有显示写构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器就不会再生成了。对于编译器生成的默认构造函数,内置类型不做处理,自定义类型会去调用其本身的默认构造函数。
cpp
class test
{
public:
test(int x =10,int y=20)
{
_x = x;
_y = y;
}
private:
int _x;
int _y;
};
构造函数也是函数,所以根据不同需求可以对构造函数进行重载。
什么时候要自己写构造函数呢??++一般情况下类成员中由内置类型的成员就需要写默认构造函数,全是自定义类型成员时,就不需要写构造函数,让编译器自己生成。++
缺省值
为了解决对内置类型,编译器不处理问题,可以自己写构造函数,也可以提供缺省值让编译器生成默认构造函数时使用。
cpp
class test
{
private:
int _x =10;
int _y =20;
};
通过给_x和_y缺省值,编译器在调用默认构造时就会将其初始化。
默认构造函数
默认构造函数实际上分为3种:1)编译器默认生成的;2)全缺省的,有缺省值且每个形参都有;3)无参数的,构造函数没有参数。
析构函数
析构函数是对类对象中资源的销毁,大多数是进行动态内存的销毁。
特征
1)析构函数,函数名前加~;
2)无参数,无返回值;
3)对象的生命周期结束时,系统会自动调用析构函数。
4)一个类只能有一个析构函数,若未定义,系统会自动生成默认的析构函数,析构函数不能重载;默认析构函数对内置类型不处理,对于自定义类型去调用其自己的析构函数。
cpp
class test
{
public:
~test()
{
//销毁
}
private:
};
什么时候需要使用析构函数??一般情况下,有动态开辟的空间,要显示写析构函数,当需要释放的资源都是内置类型的,不写析构函数。
拷贝构造函数
特征
1)拷贝构造函数是析构函数的一个重载;
2)拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错,++因为将实参赋值给形参的时候需要调用拷贝构造完成,这就会导致死循环的调用拷贝构造。++
3)但不写拷贝构造函数时,编译器会自动生成默认拷贝构造,默认拷贝构造对内置类型按照其字节数进行拷贝,对自定义类型会调用其默认的拷贝构造函数。内置类型的这种拷贝也被称为浅拷贝或值拷贝。如果类中有指针,对指针进行浅拷贝的时候就会导致,两个类对象指向同一块空间。
cpp
class Date
{
public:
//默认构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_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;
};
运算符重载
C++为增强代码的可读性,引入了运算符重载,运算符重载是有特殊函数名的函数,其也有返回值和类型。关键字operator后面接需要重载的运算符符号即可。
函数原型:返回值类型 operator 操作数(参数列表);
cpp
bool operator <(const Date& d)
{
//有隐藏的this指针,_year,_month,_day又属于this指针指向的类对象
if (_year < d._year)
return true;
if (_year == d._year && _month < d._month)
return true;
if (_year == d._year && _month == d._month&&_day<d._day)
return true;
return false;
}
以上对<的运算符重载,就可以直接实现Date类对象的比较。
要求
1)不能用一些特殊符号作为操作符,eg:@,#等;
2)重载操作符必须有一个类类型参数;
3)用于内置类型的运算符,其含义不能变,eg:重载+后还是加;
4)作为类成员函数重载时,其形参看起来比操作数少一个,因为成员函数的第一个参数为隐藏的this指针;
5).* :: sizeof ?:(三目) . 这5个关键字不能重载。
赋值运算符重载
赋值运算符重载是对两个已经初始化后的类对象进行赋值。
cpp
class Date
{
public:
bool operator =(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
以上赋值运算符重载就可以实现对不同类对象间的赋值。
日期中的运算符重载
构造函数
cpp
class Date
{
//默认构造函数
Date(int year=1, int month=1, int day=1)
{
_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;
};
日期的比较
在设置运算符重载的时候,可以直接看出运算符之间的关系,从而直接调用其他已经实现的运算符来设置新的运算符。
cpp
bool operator <(const Date& d)
{
if (_year < d._year)
return true;
if (_year == d._year && _month < d._month)
return true;
if (_year == d._year && _month == d._month && _day < d._day)
return true;
return false;
}
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
return false;
}
bool operator!=(const Date& d)
{
return !((*this) == d);
}
bool operator<=(const Date& d)
{
if ((*this) == d || (*this) < d)
return true;
return false;
}
bool operator>(const Date& d)
{
return !((*this) <= d);
}
bool operator>=(const Date& d)
{
return !((*this) < d);
}
日期+天数:+=
通过日期加天数来获取未来的某一天的日期。
cpp
int Getmonthday(int year, int month)
{
int monthday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
return 29;
return monthday[month];
}
Date& operator+=(int day)
{
_day += day;
int nday = Getmonthday(_year, _month);
while (_day > nday)
{
_day -= nday;
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
nday = Getmonthday(_year, _month);
}
return *this;
}
日期+天数:+
cpp
Date operator+(int day)
{
Date tmp(*this); //通过拷贝构造一个新类对象进行返回
tmp._day += day;
int nday = Getmonthday(tmp._year, tmp._month);
while (tmp._day > nday)
{
tmp._day -= nday;
tmp._month++;
if (_month == 13)
{
tmp._year++;
tmp._month = 1;
}
nday = Getmonthday(tmp._year, tmp._month);
}
return tmp;
}
日期-天数:-=
cpp
Date& operator-=(int day)
{
_day -= day;
_month--; //向将month-1,向前加日期
int nday = Getmonthday(_year, _month);
while (_day < 0)
{
_day += nday;
if (_day > 0)
break;
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
nday = Getmonthday(_year, _month);
}
return *this;
}
日期前置++和后置++
cpp
//前置++
Date& operator++()
{
(*this) += 1; //直接调用+=
return *this;
}
//后置加加
Date operator++(int) //为了与前置++分开,在参数中给一个int类型参数
{
Date tmp(*this);
*this += 1;
return tmp;
}
日期-日期
通过日期-日期来获得两个日期间的天数,日期-日期可以直接用计数器,每次+1看加了多少次。
cpp
//日期-日期
int operator-(Date d)
{
Date max(*this);
Date min(d);
if (max < min)
{
max = min;
min = *this;
}
int n = 0;
while (min != max)
{
min++; //直接用计数,每次++,看加多少次
n++;
}
return n;
}
内存管理
new和delete
C++中引入新的关键字new和delete来对动态内存进行管理。
C++中申请和释放单个内存空间用new和delete,多个内存空间用new[ ]和delete[ ] 。
cpp
int* pt1 = new int; //开辟一个int的动态内存空间
delete pt1;
int* pt2 = new int(10); //开辟并初始化为10
delete pt2;
int* pt3 = new int[10]; //开辟10个int类型的空间
delete[] pt3;
int* pt4 = new int[10] {1,2,3,4,5}; //开辟并依次初始化
delete[] pt4;
new对比malloc的优势是:对于自定义类型new会顺便调用类的构造函数,不需要我们手动再进行初始化了,同样delete对于自定义类型也会调用析构函数。
了解:C++是面向对象的语言,所以new失败后,不会像C语言一样用NULL返回值,C++更喜欢抛异常。
与malloc和free的区别
1)new和delete会对自定义类型调用构造和析构函数;
2)new和delete是关键字,malloc和free是函数;
3)new不用走动计算开辟空间的大小,malloc要手动计算;
4)new可以直接返回要开辟类型对应的指针,malloc返回的是void*还要强转;
5)开辟空间失败,new是抛异常,而malloc是返回NULL。
定位new
对于已经开辟好的空间,可以使用定位new来对空间调用析构函数。
定位new参与内存池搭配使用。用法如下。
cpp
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A* pa = (A*)malloc(sizeof(A));
new(pa)A(1);//new(空间地址)自定义类型(传给构造函数的参数)
return 0;
}
补充
cout和cin
cout和cin之所以能够实现类型的自动匹配,因为cout和cin库中有其对应的各个类型的重载函数,实际上也可以用运算符重载实现用cout和cin处理自定义函数。
此时就不能再间重载函数放在类中定义了,为什么???++因为类中有隐含的this指针,当还是在类中定义重载,就会导致需要d1<<cout这样使用输出,而我们希望的是cout<<d1来使用,所以就要在类外面定义重载函数。但是在外面定义重载函数就不能使用类的私有成员了,此时要通过声明友元函数来实现对类私有成员的访问。++
友元函数就是在类中声明自定义函数,其前面再加上friend;
Date的cout和cin
istream和ostream分别是cout和cin的类型。
cpp
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
using std::ostream;
using std::istream;
using std::endl;
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
}
初始化列表
在C++中类的有些成员变量是必须在初始化列表中初始化,在构造函数中是无法完成初始化的,初始化列表实际上是构造函数的一部分,是类对象成员定义的位置。
必需在初始化列表中初始化
1)引用成员变量;
2)const修饰的成员变量;
3)没有默认构造的自定义类型成员变量。
cpp
class test
{
test(int& x)
:a(x) //将a引用为x
,b(2) //将b赋值为2
{}
private:
int& a;
const int b;
};
注意:在初始化列表中初始化的顺序不是按照代码顺序进行的,而是按照类成员声明的顺序向下进行的。
静态成员变量
类中的静态成员变量和普通成员变量的主要区别在于:普通成员变量属于每个类对象,储存在对象里面,而静态成员变量属于类,是公有的存储在静态区。
静态成员需要在类中声明,在类外定义。
cpp
class test
{
private:
int a;
int b;
static int st;
};
int test::st = 0;
静态成员函数
静态成员函数与普通成员函数的区别是,其没有隐藏的this指针,所以调用静态成员函数要用类域和访问限定符实现。同时因为没有this指针,其不能访问类中的私有成员及其他的非静态成员函数。
匿名对象
当我们只是临时需要创建一个对象,只要求在这一行使用该对象时,就可以创建匿名对象。
cpp
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa(1); //有名对象
A(1); //匿名对象
return 0;
}
有名对象的生命周期是当前函数局部域,匿名对象的生命周期是当前行。
补充:匿名对象和临时变量一样,具有常性,引用时要带const修饰,当对匿名对象进行引用时,可以延长匿名对象的生命周期,生命周期变为当前函数局部域。
模板,泛型编程
在C语言中有时候因为类型不同要写多个重复的函数,C++为了解决这一问题引入了模板,泛型编程的概念。
泛型编程:编写与代码类型无关的通过代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板的格式:template<typename T1,typename T2...typename Tn>
返回值 函数名(参数列表){ }
cpp
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
以上代码就是交换函数的模板,T相当于可以自动识别各个类型。typename是定义模板参数的关键字,也可以用class。
注意:函数模板实际上只是一张蓝图,其本身不是函数;在编译器编译阶段,编译器首先会识别实参的类型来推演生成对应类型的函数以供调用。
对于类模板来说,类名和类型是不一样的。eg:stack是类名,stack<int>是类型。
模板参数匹配原则
1)一个非模板参数可以和一个同名的模板参数同时存在,并且该模板参数可以实例化为该非模板参数;
cpp
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10, b = 20;
Swap<int>(a, b); //让模板函数实例化为与会模板函数相同的函数
return 0;
}
2)对于非模板函数和同名的模板函数来说,如果其他条件都相同,在调用时会优先调用非模板函数。当模板函数能够实例出一个更匹配的,则调用模板函数。
cpp
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
template<typename T1,typename T2>
void Swap(T1& x, T2& y)
{
T1 tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10, b = 20;
double c = 1.1;
Swap(a, b); //调用非模板函数
Swap(a, c); //调用模板函数
return 0;
}