文章目录
C++入门知识
C与C++的关系
C语言是面向过程(procedure-oriented)的语言,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是面向对象(object-oriented)的语言,将一件事情拆分成不同的对象,靠对象之间的交互完成。
C与C++的关系是:C++即C Plus Plus,是C语言的扩展,文件后缀是.cpp。C程序可以在C++编译器下编译和运行,也就是说编写C++程序可以完全用C的语法去写。
1. 类的引入:从结构体到类
C语言结构体中只能定义变量,在C++中结构体内不仅可以定义变量,也可以定义函数,C++的结构体已经升级到了类(class)的概念。
struct Person {
char name[20];
int age;
char gender;
int height;
int weight;
void showInfo() {
cout << name << " - " << age << " - " << gender << endl;
}
void sleep() {
}
void washCloth() {
}
void readBook() {
}
void work() {
}
void study() {
}
};
C++中的struct可以这么做是因为需要兼容C,可以这么认为,类是结构体的升级,事实上C++更喜欢用class关键字表示一个类:
class Person
{
// 类体由成员变量和成员函数组成
};
在C++中用struct表示类与用class表示类有访问权限的区别,在后面的“类的访问权限”部分中会讲到。
2. 类的声明和定义
类有两种定义方式:
-
成员声明和定义全部放在类体中,需要注意的是:成员函数放在类体中一起声明和定义,编译器会将这个函数当成内联函数处理。
-
声明和定义分开,仅成员的声明放在类体,且是写在头文件中;而成员函数的定义是写在另外一个.cpp文件中的,推荐使用这种方式。
3. 类的作用域
类定义了一个新的作用域,在类体外定义成员时,需要使用 ::,::是作用域操作符,指明成员属于哪个类域。
// Person.h
class Person {
char name[20];
int age;
char gender;
int height;
int weight;
void showInfo();
void sleep();
void washCloth();
void readBook();
void work();
void study();
};
// Person.cpp
void Person::showInfo() {
cout << name << " - " << age << " - " << gender << endl;
}
4. 类的访问限定符
访问限定符用于确定类成员的访问权限:
- public:被public修饰的成员在类外可以被访问;
- protected:被protected修饰的成员在类外不能被访问,但可以在继承的子类中被访问;
- private:被private修饰的成员在类外不能被访问。
如果用struct声明和定义一个类,这个类中所有成员的默认访问权限为public,这是因为需要兼容C,C并没有访问限定符这个语法规则。使用class声明和定义一个类,成员的默认访问权限为private。
5. 面向对象特性之一:封装
面向对象的三大特性:封装、继承、多态。封装是通过private(私有的访问权限)来隐藏对象内部的属性和实现细节,控制哪些函数可以在类外部直接被使用,仅对外公开接口来和对象进行交互。
比如对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互。对于计算机使用者而言,不用关心内部核心部件,主板上线路是如何布局的,CPU内部是如何设计的。
class Person {
private:
char name[20];
int age;
char gender;
int height;
int weight;
public:
void showInfo();
void sleep();
void washCloth();
void readBook();
void work();
void study();
};
6. 类的实例化:对象
类是对一个事物进行描述,是一个模型,定义出一个类并没有并没有分配实际的内存空间存储它。使用一个类,需要用这个类创建对象,这个过程称为“类的实例化”。一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类的成员变量。
比如上面的Person类例子,对这个Person类实例化:
Person zhangsan;
Person lisi;
Person wangwu;
7. 计算类对象的内存大小
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?
其实计算对象的内存大小与计算结构体的大小方法一致,也就是有内存对齐的规则,可以看这篇文章计算结构体的大小了解。也就是说计算对象的内存大小,其实就是计算类的成员变量大小,不包含成员函数。成员函数不会包括在内,是因为成员函数是n个对象共用的,所以存放在公共代码区给这个类的多个对象共用。
空类比较特殊,它也有大小,占用1个字节空间。给1字节的的逻辑可能是,如果给0字节那么将毫无意义,不如给1个字节,那么空类的作用大概率只是一个占位,表示可能以后会对其进行完善。
8. 成员函数中暗藏的this指针
this指针本质上是“成员函数”的形参,当对象调用成员函数时,编译器将对象地址作为实参传递给this形参,因此this指针并不存放在对象中,而是在栈中(可能也在寄存器中,取决于编译器)。除了static成员函数(后面会提到),每个成员函数的参数中都隐藏了一个this指针,可以调试查看。
this指针的类型:类的类型* const,如Person* const,this只能在“成员函数”的内部使用。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要程序员手动传递。
下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
C.正常运行,表面存在空指针问题,但成员函数中并没有使用其它成员,仅仅只是打印一个字符串,不会导致空指针访问。
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
B.运行崩溃,成员函数中使用了成员变量,实际上是this指针访问的成员,造成了空指针。
9. 类的六个默认生成的成员函数
如果一个类中什么成员都没有,简称为空类。但其实空类并不是什么都没有,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
初始化和清理:
- 构造函数:初始化对象;
- 析构函数:清理对象中申请的动态内存;
拷贝和赋值:
3. 拷贝构造:使用同类对象创建另一个之前不存在的对象;
4. 赋值重载:把一个对象赋值给另一个已经存在的对象(就是相当于赋值最常规的用法,只不过这里的赋值是改变一个对象的值);
取地址重载:
5. 普通对象取地址重载;
6. const对象取地址重载。
常常会把构造函数、析构函数、拷贝构造函数和赋值重载函数重新去自定义以满足需求,而两个取地址重载基本不会去自定义重载,下面会顺便介绍原因。
9.1 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,在对象整个生命周期内只调用一次,构造函数并不是开空间创建对象,而是完成对象的初始化工作。C语言中经常会写一个Init()函数用于初始化,构造函数就相当于这个。
class Date {
private:
int year;
int month;
int day;
public:
Date() { // 无参构造
}
Date(int y, int m, int d) { // 带参构造
year = y;
month = m;
day = d;
}
};
int main() {
Date date1; // 通过无参构造函数初始化对象,不用跟括号。
Date date2(2024, 3, 11);
return 0;
}
构造函数特征如下:
- 函数名与类名相同。
- 无返回值(意思是不用写返回值,不是void的意思)。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数、全缺省的构造函数、编译器默认生成的构造函数都称为默认构造函数。这三个默认构造函数只能存在其中一个,因为这三个都可以不用传参,如果同时存在,调用时会产生歧义。
class Date {
private:
int year;
int month;
int day;
public:
Date() { // 与下面全缺省的构造函数存在冲突
}
Date(int y = 2024, int m = 2, int d = 22) {
year = y;
month = m;
day = d;
}
};
7.默认构造函数会对类中其它自定类型成员调用的它的默认构造函数,比如Date类中如果包含一个Time类成员:
class Time {
private:
int hour;
int minute;
int second;
public:
Time() {
cout << "Time()" << endl;
}
};
class Date {
private:
int year;
int month;
int day;
Time time;
public:
Date(int y = 2024, int m = 2, int d = 22) {
cout << "Date(int, int, int)" << endl;
year = y;
month = m;
day = d;
}
};
int main() {
Date date;
return 0;
}
在声明时给成员变量默认值
由于默认生成的构造函数不会进行有效的初始化,给的是随机值,所以C++11开始可以给内置类型(int/char/double等)成员在类中声明时给默认值,如果没有指定初始化则初始化成默认值。
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
Time _time;
};
9.2 析构函数
与构造函数功能相反,注意不是销毁对象本身,而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,就是清理那些申请了内存资源的成员,释放资源。
析构函数的特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数和无返回值类型。
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
- 析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("Stack malloc failed.");
return;
}
_capacity = capacity;
_size = 0;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
- 和默认构造函数一样,默认析构函数对自定义类型的成员调用它的析构函数。
class Time
{
public:
Time() {
cout << "Time()" << endl;
}
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
Time _time;
};
int main()
{
Date d;
return 0;
}
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。有资源申请时(malloc、new,new后面会提到),一定要重新自定义释放资源(free、delete),否则会造成资源泄漏。
9.3 拷贝构造函数
拷贝构造函数创建一个与已存在对象一样值的新对象,参数只有单个形参,该形参是对本类类型对象的引用,且一般常用const修饰,在用已存在的类型对象创建新对象时由编译器自动调用。
拷贝构造函数特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
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;
};
- 其它任何一个函数,只要形参不是传引用、返回值不是返回引用,调用该函数时编译器首先都会去调用拷贝构造。所以拷贝构造的形参必须是引用,如果不是引用,编译器器会报错,因为按传值的说法来看拷贝构造会引发无穷递归。
void Test1(const Date d) {
// 首先会先调用拷贝构造
}
void Test2(const Date& d) {
// 不会调用拷贝构造
}
Date Test3() {
Date date(2024, 1, 1);
return date; // 会先调用拷贝构造再返回
}
int main() {
Date date1;
Test1(date1);
Test2(date1);
Test3();
}
调用Test1(const Date d),参数不是引用,所以会先调用拷贝构造:
拷贝构造的参数不是引用,导致无穷递归:
- 若未显式定义拷贝构造,编译器默认生成的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)。如果类中有需要申请内存资源的成员(有malloc、new的成员),默认的拷贝构造无法完成拷贝,需要自己显示定义完成深拷贝。比如:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("Stack malloc failed.");
return;
}
_capacity = capacity;
_size = 0;
}
Stack(const Stack& rStack) {
_capacity = rStack._capacity;
_size = rStack._size;
_array = (DataType*)malloc(sizeof(DataType) * _capacity);
memcpy(_array, rStack._array, _size);
//for (int i = 0; i < _size; ++i) {
// _array[i] = rStack._array[i];
//}
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
如果使用编译器默认生成的拷贝构造传入stack1初始化stack2,stack2的array仅仅只是把stack1的array地址值拷贝过来了,意味着共用同一块内存。
自定义完成深拷贝的拷贝构造,有内存资源申请的成员,地址不一样:
为了提高程序效率,一般对象传参时,尽量使用引用类型;函数返回值根据实际场景,能用引用尽量使用引用,因为不返回引用的函数,实际上返回前都会去调用拷贝构造,这样会耗费一些时间和临时占用一部分内存空间。
运算符重载
在学习赋值重载之前,还需要了解运算符重载,因为赋值本身也是一种运算符,篇幅较长,不过很好理解。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,如operator+、operator-等。
- 不是所有运算符都能重载,常见能重载的运算符有:+、+=、-、-=、++和–(有规定如何区分前置和后置)、>、>=、<、<=、!=、=。
- 运算符重载函数必须有一个类的类型参数。
- 流插入 << 操作符、流提取 >> 操作符也可以重载。
下面以实现Date日期类操作理解运算符重载:
Date类声明:
#include <iostream>
#include <cassert>
using std::cout;
using std::endl;
int dayOfMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
class Date {
private:
int _year;
int _month;
int _day;
public:
// 全缺省的构造函数
Date(int y = 1970, int m = 1, int d = 1) {
_year = y;
_month = m;
_day = d;
}
// 拷贝构造函数、析构函数、赋值重载用编译器默认生成的
//Date(const Date& d) {}
//~Date() {}
//Date& 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);
bool operator!=(const Date& d);
// 获取某年某月的天数
int GetDayOfMonth(int year, int month) {
assert(month >= 1 && month <= 12);
// 2月并且是闰年
return month == 2
&& (year % 400 == 0
|| (year % 100 != 0 && year % 4 == 0))
? 29 : dayOfMonth[month];
}
// 日期+天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 日期-日期 返回天数
int operator-(const Date& d);
Date& operator++(); // 前置++
Date operator++(int); // 后置++
Date& operator--();
Date operator--(int);
void Show() {
cout << _year << "-" + _month << "-" << _day << endl;
}
};
Date类的成员函数定义(实现这些运算符重载函数):
bool Date::operator>(const Date& d) {
if (_year > d._year) {
return true;
}
if (_year == d._year) {
if (_month > d._month) {
return true;
}
if (_month == d._month) {
return _day > d._day;
}
}
return false;
}
bool Date::operator>=(const Date& d) {
//return *this > d || *this == d;
// this->operator>(d) || this->operator==(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->operator>=(d));
return !(*this >= d);
}
bool Date::operator<=(const Date& d) {
//return !(this->operator>(d));
return !(*this > d);
}
bool Date::operator!=(const Date& d) {
return _year != d._year
|| _month != d._month
|| _day != d._day;
//return !(*this == d);
}
/* 先实现+=,再利用已经实现的+=实现+ */
Date& Date::operator+=(int day) {
_day += day;
int dayOfMonth = GetDayOfMonth(_year, _month);
while (_day > dayOfMonth) {
_day -= dayOfMonth;
++_month;
if (_month > 12) {
_month = 1;
++_year;
}
dayOfMonth = GetDayOfMonth(_year, _month);
}
return *this;
}
Date Date::operator+(int day) {
// Date newdate = *this;
// 不是调用了赋值重载,实际是调用拷贝构造,
// 因为*this已经存在,而date开始不存在,详见区分拷贝构造和赋值重载部分。
Date newdate(*this); // 或Date newdate = *this;
newdate += day; // newdate.operator+=(day);
return newdate;
}
/* 先实现+,再利用已经实现的+实现+=(不推荐) */
//Date& Date::operator+=(int day) {
// *this = *this + day; // (*this).operator+(day);
// return *this;
//}
//
//Date Date::operator+(int day) {
// Date newdate(*this);
// newdate._day += day;
// int dayOfMonth = GetDayOfMonth(newdate._year, newdate._month);
// while (newdate._day > dayOfMonth) {
// newdate._day -= dayOfMonth;
// ++newdate._month;
// if (newdate._month > 12) {
// newdate._month = 1;
// ++newdate._year;
// }
// dayOfMonth = GetDayOfMonth(newdate._year, newdate._month);
// }
// return newdate;
//}
Date Date::operator-(int day) {
Date newdate(*this);
newdate.operator-=(day); // newdate -= day;
return newdate;
}
Date& Date::operator-=(int day) {
_day -= day;
while (_day < 1) {
--_month;
if (_month < 1) {
_month = 12;
--_year;
}
_day += GetDayOfMonth(_year, _month);
}
return *this;
}
int Date::operator-(const Date& d) {
int amountDays1 = _day;
int month = _month - 1;
for (int y = _year; y > 0; --y) {
for (int m = month; m > 0; --m) {
amountDays1 += GetDayOfMonth(y, m);
}
month = 12;
}
int amountDays2 = d._day;
month = d._month - 1;
for (int y = d._year; y > 0; --y) {
for (int m = month; m > 0; --m) {
amountDays2 += GetDayOfMonth(y, m);
}
month = 12;
}
int gapDay = amountDays1 - amountDays2;
return gapDay >= 0 ? gapDay : gapDay * -1;
}
// 前置++
Date& Date::operator++() {
*this += 1; // (*this).operator+=(1);
return *this;
}
// 后置++
Date Date::operator++(int) {
Date newdate(*this);
*this += 1;
return newdate;
}
Date& Date::operator--() {
*this -= 1;
return *this;
}
Date Date::operator--(int) {
Date newdate(*this);
*this -= 1;
return newdate;
}
重载前置++或前置–规定返回值必须是引用,其实就是提前–完返回自身。重载后置++或后置++规定返回值不能是引用(不能返回自身,因为得返回++前或–前的值),而且必须有一个int符在参数列表占位,这个参数没有实际作用,纯粹就是用来表明是重载后置++或后置–。
调用运算符重载函数可以正常像函数调用一样,也可以像正常的运算符一样使用:
Date d1(2024, 4, 12);
Date d2(2022, 4, 14);
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
cout << d1.operator>(d2) << endl;
cout << (d1 > d2) << endl;
cout << d1.operator>=(d2) << endl;
cout << (d1 >= d2) << endl;
cout << d1.operator<(d2) << endl;
cout << (d1 < d2) << endl;
cout << d1.operator<=(d2) << endl-
cout << (d1 <= d2) << endl;
cout << d1.operator!=(d2) << endl;
cout << (d1 != d2) << endl;
Date d3(d2);
Date& tempRefd3 = d3.operator+=(19);
// Date& tempRefd3 = d3 += 19;
Date d4 = d3.operator+(19);
// Date d4 = d3 + 19;
Date& tempRefd3 = d3.operator-=(9);
//Date& tempRefd3 = d3 -= 9;
Date& tempRefd3 = d3.operator-=(19);
//Date& tempRefd3 = d3 -= 19;
cout << "d1 - d2:" << d1.operator-(d2) << "天" << endl;
cout << "d1 - d2:" << d1 - d2 << "天" << endl;
d1.operator+=(100);
d2 += 2000;
Date date1 = d1.operator+(100);
Date date2 = d2 + 2000;
d1.operator-=(100);
d2 -= 563;
Date date1 = d1.operator-(100);
Date date2 = d2 - 563;
9.4 赋值重载
编译器会默认生成一个赋值重载函数,效果和下面这个自定义实现一样。
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
重点是返回引用和*this的写法。
一般情况下不需要去自定义实现,需要自定义实现的场景可以看下面的注意事项。自定义实现需要注意的是:
-
赋值运算符只能重载成类的成员函数不能重载成全局函数。因为如果不显示定义,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
-
编译器生成的默认赋值运算符重载函数,以值的方式逐字节拷贝(也就是值拷贝,常说的浅拷贝)。内置类型成员变量是直接赋值的,而自定义类型成员变量会调用对应类的赋值运算符重载完成赋值,因为自定义类型成员中究其到底也是内置类型成员。
所以由于编译器默认生成的赋值重载是浅拷贝,如果类中有成员申请内存资源,那么使用默认的赋值重载函数是不合适的,这会导致两个对象共用一块空间,这与之前的拷贝构造函数一样,需要自定义实现完成深拷贝。
区分拷贝构造和赋值重载
拷贝构造和赋值重载看起来作用是一样的,但它们有不同的作用和机制。
- 拷贝构造创建一个对象,这个对象之前是不存在的。
- 赋值重载是对一个对象赋值,这个被赋值的对象其实是已经存在的对象了,所以赋值重载的作用就是就是修改一下值而已,本身赋值的作用就是这样。
那上面实现的Date类举例,可以更好地理解他们的区别。现在还有很多人喜欢这么写,看起来是赋值重载,其实不是:
Date Date::operator+(int day) {
Date newdate = *this;
// Date newdate = *this;不是调用了赋值重载,实际是调用拷贝构造,
// 因为*this已经存在,而newdate最开始不存在。
// 所以这样写和写成拷贝构造的方式区别不大:Date newdate(*this);
newdate += day; // newdate.operator+=(day);
return newdate;
}
9.5 取地址重载(了解)
这两个取地址重载函数不用自己去定义,用编译器默认生成的就好了。非要自定义实现的话,也很简单:
class Date {
private:
int _year;
int _month;
int _day;
public:
Date* operator&() {
return this;
}
// 最后的const表示const成员,和前面那个返回值的const修饰作用不一样,后面会提到。
const Date* operator&() const {
return this;
}
}
如果连这个&引用重载都要自己去定义,那就太麻烦了,毕竟这是使用频率非常高的操作符。C++设计者做了这个语法,我估计完全就是为了完成操作符重载这一概念的逻辑闭环,确实得有这个东西,但一般很少去自己去重新定义。
除非有啥特殊的需求,就是要去重新定义,比如返回空指针、假地址、野指针之类的,但即使是这种需求感觉意义也不大,我看就是恶搞故意整一个bug。比如:
Date* operator&() {
return nullptr;
}
const Date* operator&() const {
return (const Date*)0x0012ff40;
// int a = 10;
// return (const Date*)&a;
}
最后总结默认成员函数机制
构造函数和析构函数:不处理内置类型成员,对于自定义类型成员会去调用它自己的构造函数和析构函数。
拷贝构造函数和赋值重载函数:处理内置类型成员,不过仅完成值拷贝(浅拷贝)。对于自定义类型成员会去调用它自己的拷贝构造函数和赋值重载函数。
10. const成员函数
const修饰成员函数(注意修饰的写法),实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void ShowDate() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
可以理解为这样(实际不允许这么写):
void Print(const Date* this) {
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
不要与const修饰返回值混淆:
const Date GetDate() const {
return *this;
}
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?不可以。
- 非const对象可以调用const成员函数吗? 可以
- const成员函数内可以调用其它的非const成员函数吗?不可以
- 非const成员函数内可以调用其它的const成员函数吗? 可以
这几个关于const与非const的问题,其实就是权限的问题。正常的变量、函数允许读写擦欧洲哦,而const修饰后的变量或函数只允许读操作(只读),也就是说这个const变量不能被修改const函数内不能修改任何成员的值。
权限缩小是可以的,权限放大不行。通过这个结论用几句话总结上面四个问题:const不能调用非const,非const可以调用const。
11. 再谈构造函数:初始化列表
构造函数给对象中各个成员变量一个合适的初始值,不过构造函数中函数体的语句不能称之为初始化,只能称为赋初值。初始化只能一次,而函数体内赋初值却可以多次重复。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
要想真正只初始化一次,C++构造函数提供初始化列表的语法:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
// ...
}
private:
int _year;
int _month;
int _day;
};
刚开始接触只会感觉这种写法是错误的,不应该存在这样的语法。初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
调用构造函数时首先会执行初始化列表,再是函数体内的语句。就算不写初始化列表,其实它也存在,也会执行,给每个成员变量初始化一个随机值。所以不管写不写,都是初始化列表先执行,不如写上用于初始化。
并且规定以下成员要么在声明的同时给一个默认值,要么就必须在初始化列表进行初始化:
- 引用成员变量。(说明:本来引用就要直接声明的时候给初值,所以引用成员变量必须在初始化列表初始化是理所当然的,比函数体内语句要早执行。)
- const成员变量。
- 没有默认构造函数的自定义类型成员。(默认构造函数有三种:1.编译器生成的默认构造函数;2.自定义的无参构造函数;3.全缺省参数的构造函数。这三种默认构造函数的相同点都是可以不进行传参调用)。
class A
{
public:
A(int a)
:_member(a)
{}
private:
int _member;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
再重申一次,尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
初始化列表有一个特性:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。看例子:
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();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
上面程序的运行结果,正常选恐怕都会选A,但其实答案D,上面的特性说明的很清楚了。
12. 构造函数:explicit关键字
对于单个参数,或除第一个参数无默认值、其余均有默认值的构造函数,或全缺省参数的构造函数,它们具有类型转换的作用。
class Date
{
public:
Date(int y)
:_year(y)
{}
// 或
// Date(int y, int m = 1, int d = 1)
// :_year(y)
// ,_month(m)
// ,_day(d)
// {}
// 或全缺省构造函数
private:
int _year;
int _month;
int _day;
};
用一个整形变量给日期类型对象赋值,实际编译器背后会用2023构造一个匿名对象,最后用匿名对象给d1对象进行赋值,这一流程就是类型转换。
void Test() {
Date d(2024, 1, 1);
d = 2023;
}
如果要禁止这一类型转换功能,用explicit关键字给构造函数修饰就行:
explicit Date(int y = 1970, int m = 1, int d = 1)
:_year(y)
,_month(m)
,_day(d)
{
}
13. static成员
用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。以下是使用static成员的重要注意事项:
- 静态成员变量声明的时候需要加static关键字,但不能声明的同时初始化,一定要在类外进行初始化,并且使用作用域操作符::表明该static成员变量属于哪个类。
class A {
private:
static int _count;
}
int A::_count = 0;
- 静态成员为所有类对象所共享,不属于某个具体的对象,并且存放在静态区。因此可以用类名::静态成员、或对象.静态成员两种方式访问。
cout << A::_count << endl;
A a;
cout << a._count << endl;
- 静态成员函数没有隐藏的this指针,因此不能访问任何非静态成员。
- 静态成员受public、protected、private访问限定符的影响。
面试题:实现一个类,计算程序中创建出了多少个类对象。(静态成员变量解决)
常见问题:
- 静态成员函数可以调用非静态成员函数吗?不可以,没有this。
- 非静态成员函数可以调用类的静态成员函数吗?可以,有this。
14. friend友元(友元函数与友元类)
友元提供了一种突破封装的方式,使private失效,有时这么做为了提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
14.1 友元函数(涉及重载输入>>和输出<<)
问题:现在尝试去重载operator<<和operator>>,然后发现调用时很怪,因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置,this指针默认是第一个参数也就是左操作数了。
ostream& operator<<(ostream& out) {
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
istream& operator>>(istream& in) {
cout << "请依次输入年月日:";
in >> _year >> _month >> _day;
return in;
}
只能这么调用,和原来cout、cin的使用方式反了一下:
但是实际使用中,cout是第一个形参对象才能正常使用,所以要将operator<<重载成
全局函数,但这时重载成类成员函数是不合理的:
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d) {
cout << "请依次输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
但放在类外,又会导致类外没办法访问私有成员:
此时就需要友元来解决。友元关键字friend,先用friend声明某个函数是这个类的朋友,那这个函数就可以访问该类的私有成员:
class Date { // 这里简化一下Date类,防止太多代码了看不过来。
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
}
这时访问就没有任何问题了:
这样调用起来也不会觉得像最开始那样怪怪的,cout和cin占据第一个参数:
使用友元函数的注意事项:
- 友元不是类成员:
- 友元函数可访问类的私有成员;
- 友元函数不能用const修饰;
- 友元函数不受类访问限定符限制;
- 一个函数可以是多个类的友元函数。
14.2 友元类
使用friend在一个类中声明另一个类为友元类(即声称另一个类为朋友),这个友元类可以访问本类任意成员。注意是友元类可以访问本类,而不是那个声明某个类是友元类的类可以访问友元类,友元关系是单向的,就好比你把别人当朋友,别人不把你当朋友。
class Time {
friend class Date;
private:
int hour;
int minute;
int second;
}
Time类声明Date为友元类,那么Date类中就可以随意访问Time类成员。
使用友元类需要注意的是:
- 友元关系是单向的(你把人当朋友,别人不一定把你当朋友);
- 友元关系不能传递(别人的朋友不一定也是你的朋友);
- 友元关系不能继承(你长辈的朋友不是你的朋友)。
15. 内部类
一个类定义在另一个类的内部,那么这个类就是内部类。内部类是一个独立的类,
它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
所以说内部类就是外部类的友元类,内部类可以随意访问外部类成员,而外部类成员不嗯呢该访问该内部类。
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
static int n;
class B { // B是A的友元类,可以任意访问A
public:
void Test(const A& a) {
cout << a._a << endl;
cout << n << endl;
}
};
};
16. 匿名对象
匿名对象就是没有名字的对象,那没有名字意味着后续就不能再找到这个对象去使用了,所以匿名对象的使用特点是只使用一次,它的生命周期也只有那么一行代码。
class A
{
public:
A() { // 构造
cout << "A()" << endl;
}
~A() { // 析构
cout << "~A()" << endl;
}
void Test()
{
cout << "Hello" << endl;
}
}
注意创建匿名对象的方式,不用取名字:
17. 编译器会优化构造与拷贝构造的重复调用
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。前面说过,函数传值而不传引用,实际上在调用这个函数前会先去调用拷贝构造,返回值而不返回引用也同理。
但这种机制很多时候都是多此一举的,因为构造和拷贝构造经常会多次调用,消耗无谓的时间和空间,对于这样的多次调用,编译器进行了优化:
- 调用构造 + 拷贝构造,优化成只调用一次构造;
- 调用拷贝构造 + 拷贝构造,优化成只调用一次拷贝构造或直接调用一次构造;
- 调用构造 + 拷贝构造 + 拷贝构造,优化成只调用一次构造。、
不过需要注意的是即便某些场景满足上述条件,但不会进行优化,后面会提到。
不同编译器优化程度、优化方式也不同,我这里是用的Visual studio2022工具,那么内置的是微软的msvc编译器。即使是同一种编译器也受不同版本影响,可能导致结果不同,比如我的是visual studio2022,那可能和2019/2013的优化程度就不同。与其他c++编译器,如linux下的gcc或苹果的c lang差别可能就更大了,但可以肯定的是,都大差不差有类似优化,优化方向也是一样的。另外debug环境下的优化程度也和release环境下不同,以上有这么多可能的差异,如果有小伙伴有不同的结果,不用奇怪。
以A类、然后调用f1和f2函数作为测试:
class A
{
public:
A(int a)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a) {
cout << "A(const A& a)" << endl;
}
~A() {
cout << "~A()" << endl;
}
}
void f1(A a) {
}
A f2() {
A a;
return a;
}
A f3() {
return A();
}
优化构造 + 拷贝构造为构造:
这么写没问题,单参数构造可以直接赋值。本来是要先将100构造一个临时对象,再对a进行拷贝构造。
当然下面这个场景就没办法优化了,本身就只有构造:
const A& cst_a = 100;
这也是优化构造+拷贝构造为构造的情况:
本来是先构造匿名对象,将匿名对象传值调用f1函数前会调用拷贝构造。
下面这种情况没办法优化:
这是两行表达式,不是一行,理论上也没办法去强行优化。
优化构造+拷贝构造+拷贝构造为1个构造(我记得visual studio2019好像是优化成了1个拷贝构造):
这里本来f2()返回一个临时对象要调用一次拷贝构造,然后res接收f2()返回值又要调用一次拷贝构造,然后优化的话是直接在f2()中构造a后给res了,没有再次调用拷贝构造去给res,或者可能就是调用f2()没有对a进行构造,而是直接对res构造。
这个也是优化构造+拷贝构造+拷贝构造为1个构造: