文章目录
面向过程和面向对象
类的引入
C中结构体和C++中类的对比
在C++里面定义类,可以用struct,也可以用class
C++中兼容C里面结构体的用法,同时struct在C++中也升级成了类
比如,在C中定义结构体不仅需要写类名,还要写struct,C++中的类可以直接用类名定义变量。
struct Student
{
char name[10];
int age;
int id;
};
int main()
{
struct Student s1;//兼容C的用法
Student s2;//升级到C++,可以直接用类名定义类
strcpy(s1.name, "张三");
s1.id = 1;
s1.age = 18;
strcpy(s2.name, "李四");
s2.id = 2;
s2.age = 19;
return 0;
}
另外,与结构体不同,类里面不仅可以定义变量(被称为成员变量),还可以定义函数(被称为成员方法/函数)
同时,定义出来的类变量一般不叫变量而称它们为对象。
struct Student
{
//成员变量
char _name[10];
int _age;
int _id;
//成员方法/函数
void Init(const char* name, int age, int id)
{
strcpy(_name, name);//把第二个参数字符串拷贝到第一个参数中
_age = age;
_id = id;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
struct Student s1;
Student s2;
//在C++中不再把这里的s1、s2叫做变量,而习惯叫作对象
s1.Init("张三", 18, 1);
s2.Init("李四", 19, 2);
s1.Print();
s2.Print();
return 0;
}
成员变量前面加_是一种习惯,为了和成员函数的形参相区别。
类的定义方法
按照如下格式定义
class className
{
//类体:由成员函数和成员变量组成
};//和结构体一样,花括号后面必须有分号
定义类的时候,如果把声明和定义全部放在类体中,则成员函数会被当作内联函数处理(只是相当于函数前面加了inline,但是否真的展开取决于具体情况);
声明和定义分离按如下方式处理:
以栈类为例
头文件中放声明
类的成员函数的实现放在一个.cpp文件
需要注意的是,实现成员函数的时候,要在返回类型和函数名之间指明类域
(类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。)
类的访问限定符和封装
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类和对象的区别(实例化)
class定义的是类,相当于模板,用这个类定义一个变量(对象)的过程就是类的实例化。
类的大小的计算
类中的成员函数不存放在类中而是存放在代码段中,因此计算类的大小不需要考虑成员函数,只需要考虑成员变量。
类和C中的结构体一样需要内存对齐,规则也一致,参见C语言结构体
需要注意的是,空类(即一个类体中什么也没有的类)也需要占用1字节空间
this指针
成员函数不保存在类里面,当类实例化成对象后,对象只保存了成员变量,而成员函数是公共的,不同的对象使用的是同样的成员函数。这通过this指针实现。
实际上每个成员函数的第一个参数都是一个指向这个类的指针(这个指针的名字为this)。
成员函数里面的成员变量都会被编译器修改成this->成员变量。
关于this的使用规则
1.调用成员函数时,不能显式地传实参给this
2.定义成员函数时,也不能显式声明形参this
3.但是在成员函数内部可以显式用形参this,但是一般除非必要都不写this
this指针并不存在类中(计算类的大小时不考虑this指针),一般存在栈中,但也可能是在寄存器中。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;//假如成员变量和形参同名year = year,则两个year都是形参,因为优先访问局部变量
_month = month;//当然也可以写成Date::year = year,指定清楚类域,但是不建议
_day = day;
}
//void Init(Date* const this, int year, int month, int day)
//{
// this->_year = year;
// this->_month = month;
// this->_day = day;
//}
//注意只有成员变量才会被加上this->
//实际上会被编译器处理成void Print(Date* const this)
//this指向的地址不能改变,指向地址中的内容可以改变
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
//cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 1, 15);//d1.Init(&d1, 2022, 1, 15);
d1.Print();//这句代码会被编译器处理成d1.Print(&d1);
//所有的非静态成员函数都会被处理(都隐藏了this指针)
Date d2;
d2.Init(2022, 1, 16);
d2.Print();//d2.Print;
//需要注意的是,
//1.调用成员函数时,不能显式地传实参给this
//2.定义成员函数时,也不能显式声明形参this
//3.但是在成员函数内部可以显式用形参this,但是一般除非必要都不写this
//this一般存在栈里,不过VS是存在寄存器ecx中的
return 0;
}
进一步理解成员函数是如何调用的
class A
{
public:
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();//程序会正常运行
return 0;
}
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();//这里运行崩溃
return 0;
}
这是因为当用类指针去调用成员函数的时候,编译器根据指针类型找到成员函数进行调用,并没有对这个指针进行解引用。
但是如果在类里面出现了成员变量,则就会用这个指向对象的this指针解引用得到具体的成员变量。这时才会出现对空指针的解引用,程序报错。
类的6个默认成员函数
任何一个类,对于下面6个成员函数,如果我们不自己实现,编译器也会生成一份(如果自己写了,编译器就不再生成)
分别是
- 构造函数
- 析构函数
- 拷贝构造
- 赋值重载
- 取地址重载
- const取地址重载
构造函数
构造函数是用来初始化成员变量的成员函数。
特征
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
例如下面的成员函数Date()就是构造函数,构造函数不需要我们自己去调用,当我们实例化出一个对象时会自动调用构造函数。
这里的例子的构造函数是自己写的构造函数,可以接收参数,传参方法是实例化对象的时候在对象名后面加括号,参数写在括号里面。
但是不能这样写
Date d();
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)//这里和上一个构造函数构成函数重载
{
_year = year;
_month = month;
_day = day;
}
//上面两个函数其实可以写成一个函数,利用缺省参数的语法
//推荐使用全缺省或者半缺省
//Date(int year = 1, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//注意,从语法上,全缺省和无参的两个函数可以构成函数重载
//只要不使用无参调用没有问题,但是如果存在无参调用则存在二义性
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//没有参数,调用第一个构造函数
Date d2(2022, 1, 15);//调用有参数的构造函数的方法
Date d3(2020);
return 0;
}
如果我们不自己实现构造函数,编译器会自动生成一个无参的构造函数(如果我们自己实现了构造函数,编译器将不在生成),但是对于内置类型(int、char等)不会初始化,对于自定义类型(类)会去调用这个类的构造函数。
默认构造函数
不用参数就可以调用的构造函数就是默认构造函数(没有缺省参数就不能没有参数调用,参见C++入门缺省参数知识点)。
包括无参的构造函数、全缺省的构造函数。
默认构造函数只能有一个(否则调用时会出现歧义)
析构函数
析构函数完成资源清理工作,相当于C中调用free释放在堆中开辟的空间。
但是即使这个类中没有变量存在堆上,也需要有析构函数。
对象在销毁时会自动调用析构函数,完成类的资源清理工作。
特征
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
默认生成的析构函数对于内置类型不做处理,对于自定义类型会去调用它的析构函数。
对于没有动态开辟空间的类,用编译器自己生成的析构函数就行了;如果有动态开辟的空间,则需要自己写析构函数来释放空间,否则会造成内存泄漏。
以栈对象为例
class Stack
{
public:
Stack(int capacity = 4)//只给需要的参数就行了
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{}
//如果不自己写默认构造函数,默认生成的析构函数和默认生成的构造函数相似
// 1.对于内置类型成员变量不处理(这里是有原因的,如果都处理可能会误杀)
// 2.对于自定义类型成员变量会去调用它的析构函数
//
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
注意一个小细节:析构的顺序是和构造的顺序相反,因为栈是后进先出的。
拷贝构造
特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。(C语言中不存在传结构体就会无穷递归,C传值时直接字节序拷贝)
class Date
{
public:
Date(int year = 1900, 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;
};
如上代码中第二个成员函数就是拷贝构造函数,有了拷贝构造函数就可以这样对变量进行初始化
int main()
{
Date d1;
Date d2(d1);//d2是一个和d1内容相同的对象
return 0;
}
我们自己不写拷贝构造编译器也会自己生成,自己生成的拷贝构造会进行字节序的拷贝(和strcpy一样一个一个字节的拷贝,这样的拷贝也叫浅拷贝)
对于日期类这种类浅拷贝足够了,但是对于例如栈类,浅拷贝会到来问题。
例如
class Stack
{
public:
Stack(int capacity = 4)//只给需要的参数就行了
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
上面这个程序会报错,因为s2和s1中的_a指向的是同一块空间,在s1和s2各调用一次析构函数,同一块空间被free了两次,导致报错。
而且两个不同的栈用同一块空间本身就是有问题的。
因此在这种情况下才需要自己实现拷贝构造函数。
细节:拷贝构造也属于构造函数,当自己写了拷贝构造后,编译器将不再生成构造函数
赋值运算符重载
在讲赋值运算符重载前需要先讲什么是运算符重载
运算符重载
注意事项
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,和其他成员函数一样,第一个参数同样为隐含的this指针
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
由于重载函数一般需要访问成员变量,而成员变量一般都要被限定为私有,为了不破坏封装性,一般把运算符重载为成员函数。
// 全局的operator==
class Date
{
public:
Date(int year = 2000, int month = 1, int day =1)
{
_year = year;
_month = month;
_day = day;
}
//如果是全局的运算符重载,要想能访问到成员变量。这里把成员变量设置为公有
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1;
Date d2(2022, 1, 16);
cout << (d1 == d2) << endl;//因为流插入<<优先级比==高,因此要加括号
return 0;
}
如果写成成员函数就是
class Date
{
public:
Date(int year = 2000, int month = 1, int day =1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year//实际上是this->_year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
};
int main()
{
Date d1;
Date d2(2022, 1, 16);
cout << (d1 == d2) << endl;//因为流插入<<优先级比==高,因此要加括号
return 0;
}
注意上面写成成员函数后只有一个参数了,这是因为第一个参数是隐含的this指针。
回到赋值运算符重载
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//Date operator=(const Date& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
// return *this;//这里是传值返回,可以自己写拷贝构造看调用了几次拷贝构造
//}
Date& operator=(const Date& d)
{
if (this != &d)//小优化,注意不能写成*this != d,
//因为这样要重载!=且要调用函数,就不是优化了
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;//由于函数结束时*this还没有销毁,所以可以引用返回
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 16);
Date d2;
d2 = d1;
return 0;
}
注意几个小细节,由于不需要对参数进行修改,使用const引用传参,由于函数结束时,*this所指内容没有销毁,所以可以使用引用返回(参见C++入门)。if判断是在已经相等时不再赋值(判断的花销比赋值大)。
取地址操作符重载、const取地址操作符重载
将这两个默认成员函数之前需要先讲const成员函数
const成员函数
const修饰的类成员函数称为const成员函数,作用是使得this指针的类型为
const 类* const
这样就可以使用const类型的类对象使用成员函数。(建议将不需要修改类对象内容的成员函数设置为const成员函数)
const成员函数定义方法:成员函数括号后面加const
class Date
{
public:
void Print() const
{
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
回到取地址操作符重载、const取地址操作符重载
class Date
{
public:
//取地址操作符重载(对非const
Date* operator&()
{
return this;
}
//const取地址操作符重载(可以对const类对象取地址,返回地址类型也是const类型指针)
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
但是这两个成员函数一般不需要自己写,默认生成的就够用了。(也就是不写类的取地址运算符重载也可以对类对象取地址)
实现一个日期类
构造函数
(这里是声明定义分离的写法,因此指明类域)
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!(year > 0
&& (month > 0 && month < 13)
&& (day > 0 && day <= GetMonthDay(year, month))))
{
cout << "非法日期->";
Print();//再类里面也可以调用成员函数,会转换成
//this->Print();
}
}
打印日期
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
两个日期比较大小>
bool Date::operator>(const Date& d) const//实现大于(或小于)和等于,其他比较符号的重载全部复用
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
else
{
return false;
}
}
两个日期相等==
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
>=
bool Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
}
<
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
<=
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
!=
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
日期加上/减去天数返回日期
要实现这个函数需要知道给定年份每个月的天数,因此先实现一个函数GetMonthDay()
(这里是声明定义分离的写法,因此指明类域)
int Date::GetMonthDay(int year, int month)
{
//每次进来这个数组都不变,可以定义成静态的,小优化
static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = monthDayArray[month];
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{//先判断是不是2月再判断是不是闰年
//注意条件判断与左边要加括号,严格按照逻辑完成每一步
//假如写成这样
//(month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
//则从左往右依次判断,对于那些不是2月但是是400年整数倍年份的年的每个月都加了一天(2000年)
day += 1;
}
return day;
}
日期加上天数返回日期
Date& Date::operator+=(int day)//函数结束*this还存在,可以引用返回
{
if (day < 0)//处理特殊情况
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
同理
日期减去天数返回日期
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;//注意要先处理月
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);//再处理日
}
return *this;
}
两个日期相加/相减(注意和加等/减等的区别)
Date Date::operator+(int day) const//函数结束ret被销毁,不能引用返回
{
Date ret(*this);
ret += day;//复用加等
return ret;
}
Date Date::operator-(int day) const//日期减天数
{
Date ret(*this);
ret -= day;
return ret;
}
给定一个日期,打印这天是星期几
void Date::PrintWeekDay() const
{
const char* arr[] = { "星期一","星期二","星期三","星期四","星期五","星期六","星期天" };
Date start(1900, 1, 1);//这个日期为星期一
int count = *this - start;//这里假如两个日期-的重载没有设置成const成员函数,则*this就没法作为参数调用这个重载函数
//int count = *this - Date(1900, 1, 1);//这里用匿名函数
//cout << count << endl;
cout << arr[count % 7] << endl;
}
日期++/–
为了区别前置++和后置++,C++规定
前置++的重载的无参,后置++参数列表为一个int(这里不考虑隐含的this指针)
前置++
Date& Date::operator++()//这里可以用引用返回
{
*this += 1;
return *this;
}
后置++
Date Date::operator++(int)//这里要用传值返回,存在两次拷贝构造
{
Date ret(*this);//拷贝构造
*this += 1;
return ret;//又一次拷贝构造
}
同理
前置–
Date& Date::operator--()
{
*this -= 1;
return *this;
}
后置–
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
日期减日期返回天数(和前面日期减天数返回日期构成函数重载)
int Date::operator-(const Date& d) const//日期减日期
{
Date max = *this;
Date min = d;
int flag = 1;//假设*this大于d
if (*this < d)
{
flag = -1;
max = d;
min = *this;
}
int count = 0;
while (min != max)
{
++min;
++count;
}
return count * flag;
}
流插入/流提取函数重载
首先要知道cout本质是ostream类的一个类对象
<<自动识别别类型本质是函数重载
假如按照之前的运算符重载一样把<<重载为成员函数
(返回值是ostream&是为了实现连续输出,cout这个对象在函数销毁后仍存在,因此可以引用返回。
ostream& Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
}
但是这时用<<只有以下两种方法
int main()
{
Date d1(2022, 1, 17);
d1.operator<<(cout);
d1 << cout;
return 0;
}
其中第一种用法太麻烦,第二种用法不符合常规用法,使得这个运算符重载并没有实现运算符重载提高代码可读性的初衷。
出现这种问题的原因是成员函数的第一个参数已经被固定为指向对象的this指针。
解决办法是把<<重载成全局函数,这样能自己设置参数的顺序,但是函数内要访问成员变量又尽量不破坏封装性,需要用到友元。
友元包括友元函数和友元类,这里先讲友元函数
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
回到流插入/流提取函数重载
.h中
class Date
{
//友元函数(声明要放到类里面)
//friend void operator<<(ostream& out, const Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//略
private:
int _year;
int _month;
int _day;
};
.cpp中
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year;
in >> d._month;
in >> d._day;
return in;
}
友元函数的注意事项
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
关于析构函数的作业题
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class C
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
};
class D
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
};
C c;
int main()
{
A a;
B b;
static D d;
//构造顺序:C A B D
//析构顺序:B A D C
//局部静态变量比全局静态变量先销毁
//局部变量比全局变量先析构
//如果同为局部或全局(生命周期相同的),都是先定义的后析构
return 0;
}
构造和拷贝构造连续发生时,合并为只有一次拷贝构造。
拷贝构造有如下两种调用方式
Widget v(u);
Widget w = v;//注意这里不是调用赋值重载,这里创建对象w用的是拷贝构造函数
函数的返回值(传值返回)没有接收时,会有一次拷贝构造,把返回值先拷贝给一个临时变量。(包括像下面这样的两数相加的表达式的结果也会拷贝保存在一个临时变量)
a+b;
匿名函数
如果需要用一个类对象(比如来传参)但是不实例化一个类,可以用匿名函数。
匿名函数的生命周期只在它所在的那一行。
Widget();
下面过两种方法都可以用来调用函数
Widget x;
f(x);
f(Widget());
下面来看例题
class Widget
{
public:
Widget()
{
cout << "Widget()" << endl;
}
Widget(const Widget&)
{
cout << "Widget(const Widget&)" << endl;
}
Widget& operator=(const Widget&)
{
cout << "Widget& operator=(const Widget&)" << endl;
return *this;
}
~Widget()//验证匿名函数的生命周期
{
cout << "~Widget()" << endl;
}
};
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
int main()
{
Widget x;
Widget y = f(f(x));//如果没有优化,则一共有9次拷贝构造;优化后变成7次
return 0;
}
初始化列表
构造函数其实完成的工作不是初始化,而是赋初值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
要完成初始化实际是通过初始化列表实现的。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)//初始化列表
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表是必要的,因为对于某些成员变量必须初始化,而不能先定义再赋初值。
当成员变量有以下三种时,必须对它们用初始化列表来初始化
- 引用成员变量
- const成员变量
- 没有默认构造函数的自定义类型成员
此外,每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
初始化列表可以只对部分成员进行初始化
class A
{
public:
A(int a)
{
_a = a;
}
private:
int _a;
};
class Date
{
public:
//Date(int year, int month, int day)
//{
// //这里报错说明到构造函数体内时,成员变量已经定义出来了
// _N = 10;
// _year = year;
// _month = month;
// _day = day;
//}
//初始化列表——成员变量定义的地方
//不能重复初始化,可以只有部分成员变量初始化
//只有构造函数才能用初始化列表(包括构造函数和拷贝构造)
//Date(int year, int month, int day)
// :_year(year)
// ,_month(month)
// ,_day(day)
// ,_N(10)//这时相当于全部都在定义的时候初始化
//{}
//也可以只初始化部分变量
Date(int year, int month, int day,int i)
:_N(10)
,_ref(i)
,_aa(-1)
{}
//总之,必须在定义的时候初始化的变量必须在初始化列表初始化
//有三个类型的变量必须在初始化列表初始化
//1.const类型
//2.引用
//3.没有默认构造函数的自定义类型
private://这里是声明,不是定义,声明不能初始化!
int _year;
int _month;
int _day;
//怎么初始化常变量?
//常量必须在定义的时候初始化
const int _N;//const
int& _ref;//引用
A _aa;//没有默认构造函数的自定义成员变量
};
//函数的定义是实现函数,变量的定义是开空间
int main()
{
int i = 0;
Date d1(2022, 1, 19, i);
//Date d2(2022, 1, 19);
return 0;
}
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class Date
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
}
class A
{
public:
A(int a = 0)
{
cout << "A(int a = 0)" << endl;
_a = a;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
_a = aa._a;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
_a = aa._a;
return *this;
}
private:
int _a;
};
class Date
{
public:
//Date(int year, int month, int day, const A& aa)
//{
// _aa = aa;
// _year = year;
// _month = month;
// _day = day;
//}
Date(int year, int month, int day, const A& aa)
:_aa(aa)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
//A aa(10);
Date d1(2022, 1, 19, aa);
return 0;
}
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
explicit关键字
class Date
{
public:
//explicit Date(int year)
// :_year(year)
//{
// cout << "Date(int year)" << endl;
//}
Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}
private:
int _year;
};
int main()
{
//下面两个虽然都是直接构造,但是过程是不一样的
Date d1(2022);
Date d2 = 2022;//隐式类型转换
//本意是用2022构造一个临时变量Date(2022),再用这个对象拷贝构造d2
//但是C++编译器再连续的一个过程中多个构造会被优化,合二为一
//所以这一被优化为直接就是一个构造(新编译器一般都会做)
//C隐式类型转换-相近类型,表示意义相近的类型
//中间会生成临时变量
double d = 1.1;
int i = d;
//const int& i = d;//这一句会报错,因为不是相近类型
//强制类型转换-无关类型
int* p = &i;
int j = (int)p;
return 0;
}
如果不用explicit关键字
static成员
以计算一个程序种创建了多少个类对象的场景为例
int count = 0;
class A
{
public:
A(int a = 0)
:_a(a)
{
++count;
}
A(const A& aa)
:_a(aa._a)
{
++count;
}
private:
int _a;
};
void f(A a)
{
}
int main()
{
A a1;
A a2;
f(a1);
cout << count << endl;
return 0;
}
如果要不使用全局变量,则可以用静态成员变量
class A
{
public:
A(int a = 0)
:_a(a)
{
++_sCount;
}
A(const A& aa)
:_a(aa._a)
{
++_sCount;
}
//int GetCount()
//{
// return _sCount;
//}
//静态成员函数,没有this指针,且只能访问静态成员变量和成员函数
static int GetCount()
{
return _sCount;
}
private:
int _a;
//public:
//静态成员变量属于整个类,生命周期在整个程序运行期间,作用域在类域
static int _sCount;//声明
};
int A::_sCount = 0;//定义+初始化
//注意静态成员的定义方式,只能这样定义
//静态成员不属于任何一个对象,因此不能用构造函数定义
void f(A a)
{
}
int main()
{
A a1;
A a2;
f(a1);
//cout << count << endl;
//当count是公有时,可以这样访问
//在类外面访问
//cout << A::_sCount << endl;
//cout << a1._sCount << endl;
//cout << a2._sCount << endl;
//当_sCount为私有
//cout << a1.GetCount() << endl;
//改成静态成员函数后,可以用对象去调,也可以用类去调
cout << a1.GetCount() << endl;
cout << A::GetCount() << endl;
return 0;
}
静态成员函数不能调用非静态成员变量和非静态成员函数
但是非静态成员函数可以调用静态成员函数
一道关于静态成员变量和构造函数应用的例题
等差数列求和
class Sum
{
public:
Sum()//每次调用构造函数加一次
{
_ret += _i;
++_i;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//这里用了变长数组
//return Sum().GetRet()-(n+1);//使用匿名函数来调GetRet,多调用了一次所以要减去n+1
return Sum::GetRet();
}
};
C++成员初始化的新方法
class B
{
public:
B(int b = 0)
:_b(b)
{}
private:
int _b;
};
class A
{
public:
//如果在初始化列表阶段没有对成员变量初始化,就会用缺省值初始化
A()
{}
private://这里不是初始化,因为这里是声明;这里实际上是给缺省值。这里的语法是C++11以打补丁的方式添加的
int _a1 = 0;
B _bb1 = 10;//隐式类型转换
B _bb2 = B(20);//匿名函数
int* p = (int*)malloc(4 * 10);
int arr[10] = { 1,2,3,4,5,6 };
//静态的不能这样给缺省值,必须在类外面全局位置定义初始化
//static int _sCount = 0;
};
int main()
{
A aa;
return 0;
}
友元
友元包括友元函数和友元类
友元类
例如日期类Date是时间类Time的友元,则Date的成员函数可以访问Time的私有成员变量。
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
:_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)
{}
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;
};
int main()
{
return 0;
}
友元类的注意事项
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。 - 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
内部类
要点:
- 内部类是外部类的友元,但外部类不是内部类的友元。
- 内部类可以定义在外部类的public、protected、private。
- 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- 计算大小时,内部类和外部类是独立的。
class A
{
private:
static int k;
int h;
public:
//1.内部类B和在全局类定义基本一样,只是B受外部类A类域的限制(体现在经计算大小是A和B是独立的)
//2.内部类B天生就是外部类A的友元
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
private:
int _b;
};
//void f(B bb)
//{
// //A不是B的友元,不能访问B
// bb._b;
//}
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
//访问B这个类要指定类域(前提是B是公有的)
A aa;
A::B bb;//定义了一个属于B这个类的对象
return 0;
}
作为一个内部类的例子
之前的1+2+3+…的题目也可以把Sum这个类写成Solution类的内部类
题目链接
class Solution {
private:
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
private:
static int _i;
static int _ret;
public:
int Sum_Solution(int n)
{
Sum a[n];//这里用了变长数组
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;