前言:
本篇文章我们先对之前未完成的内容进行补充,之后还有很多重磅内容,我们都需要去了解,废话不多说,开始吧。
类的默认成员函数(补档):
之前我们只介绍了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;
}
关于它的更多内容,我们后续再讲。
总结:
我们要多去使用才能更好的掌握,加油吧各位!