前言:
本篇文章我们先对之前未完成的内容进行补充,之后还有很多重磅内容,我们都需要去了解,废话不多说,开始吧。
类的默认成员函数(补档):
之前我们只介绍了4个,一共有6个,那么今天我们就来把剩余两个介绍一下。
取地址重载:
class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 取地址重载Date* operator&(){return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{Date d1;const Date d2;cout << &d1 << endl;return 0;
}
这个其实很鸡肋,没啥用,我们平时使用编译器生成的就行了。
const取地址操作符重载:
// const取地址重载
const Date* operator&()const
{return this;
}
这个和取地址操作符重载没有区别。
友元:
我们之前在日期类中直接使用了但是没有详细讲解。
在C++中,友元(friend)是一个特殊的关键字,它允许某个函数或另一个类访问当前类的私有(private)和保护(protected)成员。友元关系可以是单向的,即被声明为友元的函数或类可以访问当前类的私有成员,但当前类不能访问友元的私有成员。
只需要在类的内部添加上类外定义的函数的声明,并在声明前加上关键字friend即可,一般这种友元函数允许写在类内部的任意地方,一般来说会把它放在整个类的开头。 当一个函数成为一个类的友元,那么这个函数内部就可以随意使用类中的私有(private)或保护(protected)成员了。
当时我们为了去访问私有成员,所以我们将这两个全局函数设置为Date类友元。
友元函数:
我们上面就已经设置了友元函数。
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元类的一些特性:
- 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承。在继承位置再给大家详细介绍。
我们给出代码方便理解:
class Time
{// 声明Date类为Time类的友元类,则在Date类中就可以直接访问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;
}
也就是说,A是B的友元,A中就可以直接访问B。注意友元的关系是单向的。
初始化列表:
这是本篇的王炸,大家做好迎击准备(一段神秘的呓语:su gu mu kae u tsu jun bi wo)!
C++的初始化列表(Initializer List)是构造函数的一种特性,用于初始化类的数据成员。在构造函数体执行之前,初始化列表会先执行,确保数据成员在构造函数体开始执行之前就已经被正确地初始化。
初始化列表的使用:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
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;
};
这两段内容使其目的和结果都是相同的,但是本质上是有区别的。
我们为了方便讲解,使用一个题目来说明:用栈实现队列的代码来讲解。
class stack
{
public:stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);_size = 0;_capacity = capacity;}void push(int x){_a[_size++] = x;}private:int* _a;int _size;int _capacity;
};
class MyQueue
{
private:stack _pushst;stack _popst;int _size;
};
这里有一个大家可能会忽视的问题,就是stack类中已经有了默认构造函数。
方便复习,重要的事情说三遍!
默认构造函数只能有一个。注意:无参构造函数、全缺省函数、编译器默认生成构造函数,都可以认为是默认构造函数。
所以此时stack中已经有了默认构造函数,那么接下来我们如果再stack类中没有默认构造参数,必须提供一个值才能正常使用,这是该怎么办呢?
class stack
{
public:stack(int capacity){_a = (int*)malloc(sizeof(int) * capacity);_size = 0;_capacity = capacity;}void push(int x){_a[_size++] = x;}private:int* _a;int _size;int _capacity;
};
class MyQueue
{
private:stack _pushst;stack _popst;int _size;
};
以上代码会报错,因为默认构造无法生成。 此时只能在MyQueue中显示的写构造了。此时stack没有提供默认构造,只能使用初始化列表进行初始化!
class stack
{
public:stack(int capacity){_a = (int*)malloc(sizeof(int) * capacity);_size = 0;_capacity = capacity;}void push(int x){_a[_size++] = x;}private:int* _a;int _size;int _capacity;
};
class MyQueue
{
public:// stack 不具备默认构造,只能MyQueue显示的写构造// 此时只能使用初始化列表MyQueue(int n = 20): _pushst(n), _popst(n), _size(0){}
private:stack _pushst;stack _popst;int _size;
};int main()
{MyQueue q(10);return 0;
}
其本质可以理解为每个对象中成员定义的地方。
这里我们可以发现,其实我们不在初始化列表中初始化也可以,但是有3个例外。
以下三种类的成员,必须放在初始化列表的位置进行初始化:
- 引用成员变量(因为要先有具体变量,才能有引用)
- const成员变量(因为只有一次赋值机会)
- 自定义类型成员(且没有默认构造函数)
我们以代码的形式进行说明:
class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:B(int a, int& ref):_aobj(a), _ref(ref), _n(10){}
private:A _aobj; // 没有默认构造函数int& _ref; // 引用const int _n; // const
};
建议:能在初始化列表中初始化就在初始化列表中初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
还没完,王炸怎么可能就这点,观察以下代码:
class stack
{
public:stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);_size = 0;_capacity = capacity;}void push(int x){_a[_size++] = x;}private:int* _a;int _size;int _capacity;
};
class MyQueue
{
public:// stack 不具备默认构造,只能MyQueue显示的写构造// 此时只能使用初始化列表MyQueue(){_size = 0;}
private:stack _pushst;stack _popst;int _size;
};int main()
{MyQueue q;return 0;
}
我们调试一下:
所以你无论写不写,对于自定义类型,都会在初始化列表中调用它的默认构造(注意,此时stack类中有默认构造,没有默认构造会报错)。
我们之前将可以给成员缺省值,这个缺省值其实就是给初始化列表用的。class stack { public:stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);_size = 0;_capacity = capacity;}void push(int x){_a[_size++] = x;}private:int* _a;int _size;int _capacity; }; class MyQueue { public:// stack 不具备默认构造,只能MyQueue显示的写构造// 此时只能使用初始化列表MyQueue(){} private:stack _pushst;stack _popst;//此时给定了缺省值int _size = 0; };
如果此时在初始化列表中给定了_size的值,那么缺失值讲不起作用,比如:
MyQueue(): _size(5) {}
此时我们要注意,一定是先走初始化列表,之后再走函数体,所以效率会提高。所以实际中我们尽量使用初始化列表初始化。
我们来看一道面试题:
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;
}
这是为啥?因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关!
这句话很重要。因为先声明的是_a2,所以在初始化列表中会先执行_a2(_a1),之后执行_a1(a),所以_a2为随机值。
初始化列表的特点:
- 初始化列表,不管写没写,每个成员变量都会走一遍,而且在初始化列表中只能出现一次(初始化只能初始化一次)
- 对于自定义类型,会调用默认构造(没有默认构造则报错)。
- 先走初始化列表,再走函数体。
- 拷贝构造也有初始化列表。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
隐式类型转换:
之前我们讲过,不同类型的内置类型变量在相互赋值时会有隐式类型转换。
double a = 10.5;
int b = a;
就如上面这个简单的赋值,在a赋值给b之前,会产生一个临时变量,最终赋给b值的就是这个临时变量。
当将不同类型的变量取引用时,需要加const的原因,是因为临时变量具有常性。
double a = 10.5;
// int& b = a;// 报错
// int& c = 10;// 报错
const int& b = a;// 正常运行
const int& c = 10;// 正常运行
上述代码中b取的就是a产生的临时变量的引用,临时变量存储在内存的静态区,具有常性,就跟第四行代码的数字10性质是一样的,当你加上const时,这种引用权限就被放开了,因为const确保了你不会对静态区的变量做出改动。对于C++的自定义类型,与内置类型遵循的规则是一样的。
单参数构造:
C++支持一种类型转换式的构造:
class A
{
public:A(int a):_a(a){}private:int _a;
};int main()
{A aa1(1);A aa2 = aa1;//拷贝构造//隐式类型转换A aa3 = 3;return 0;
}
这里是内置类型转换为自定义类型。这里是单参数构造函数可以这样。至于用处嘛,看以下代码:
class A
{
public:A(int a):_a(a){}private:int _a;
};class Stack
{
public:void Push(const A& aa){//...}//...
};int main()
{Stack st;A a1(1);st.Push(a1);//这样写很冗余st.Push(2);//可以直接这样写return 0;
}
这样写就很爽。
多参数构造:
至于多参数构造,需要换一种写法。
class A
{
public:A(int a1, int a2):_a1(a1),_a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}private:int _a1;int _a2;
};int main()
{A a1 = { 3, 2 };A a2{ 4, 5 };//这两者等价,但是不建议下面这样写a2.Print();return 0;
}
explicit关键字:
这个知识点稍稍提一下,如果不想允许构造时出现类的隐式类型转换,可以在拷贝构造前加个explicit关键字,就可以成功限制类的隐式类型转换了。
class A
{
public://此时就限制了隐式类型转换explicit A(int a):_a(a){}private:int _a;
};int main()
{A a1 = 3;return 0;
}
关于它的更多内容,我们后续再讲。
总结:
我们要多去使用才能更好的掌握,加油吧各位!