先赞后看养成好习惯
//类型 &引用名 = 已定义的变量名
#include
using namespace std;
int main()
{
int i = 10;
int &j = i;
cout << "i = " << i << " j = " << j << endl;
cout << "i的地址为 " << &i << endl;
cout << "j的地址为 " << &j << endl;
return 0;
}
主要:
进一步说明:
相关面试问题:
50.引用和指针的区别
引用是一个变量的别名,指针是一个变量,存的另一个变量的地址。
引用不能为空引用,指针可以是空指针。
引用赋值之后不可以改变指向,指针可以改变。
引用不能单独存在,指针可以。
引用必须进行初始化,指针可以随时初始化。
51.函数参数传递中值传递、地址传递、引用传递有什么区别?
值传递,形参的改变不影响实参,实参只是将值传递过去,实际上实参的地址与形参的地址还是两块地址,所以改变其中一个,不会影响另一个。
地址传递,形参的改变会影响实参,形参中存的是实参的地址,改变形参中的值,实际上是改变它存的那块地址里的值。
引用传递,形参的改变也会影响实参, 形参是实参的别名,形参和实参是同一个地址,所以改变一个必然会影响另一个。
static 关键字的作用
53.const关键字有什么作用?
const修饰之后变为“只读”。
变量被const修饰变为常量,值被初始化后,不能再修改。
内联函数
在函数名前冠以关键字inline,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:
void Drawcircle(int r = 0, int x = 0, int y = 0);
void Drawcircle(int r);
Drawcircle(20);
可用强制类型转换将不同类型的数据进行转换。例如,要把一个整型数(int)转换为双精度型数(double),可使用如下的格式:
int i = 10;
double x = (double)i;
或
int i = 10;
double x = double(i);
以上两种方法C++都能接受,建议使用后一种方法。
通常把具有共同属性和行为的事物所构成的集合称为类。
类的对象可以看成该类类型的一个实例,定义一个对象和定义一个一般变量相似。
class 类名{
public:
公有数据成员;
公有成员函数;
protected:
保护数据成员;
保护成员函数;
private:
私有数据成员;
私有成员函数;
};
1.对一个具体的类来讲,类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
2.private: 只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
protected: 只能被类成员函数、子类函数及友元访问,不能被其他任何访问,本身的类对象也不行。
public: 能被类成员函数、子类函数、友元访问,也能被类的对象访问。
在没有继承的情况下,protected跟private相同。在派生类的时候才出现分化,子类的成员函数不能直接访问父类的private成员,只有通过父类的public函数来间接访问,不是很方便;一个类, 如果希望,它的成员, 可以被自己的子类(派生类)的成员函数直接访问,但是,又不想被外部访问那么就可以把这些成员定义为 protected访问权限。
3.类声明中的关键字private、protected、public可以任意顺序出现。
4.若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
5.c++11标准之后可以在类内给数据成员赋初始值,虽然在一定程度上这样写破坏了类的抽象性,但是却能带来一定的便利。但这也是抽象出来共有的属性啊,并且这样写后,初始化列表初始化的变量值覆盖掉声明时初始化的值;构造函数中初始化的值又会覆盖掉初始化列表的值。
构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。
拷贝构造函数具有以下特点:
class Score
{
public:
Score(int m, int f); //构造函数
Score();
Score(const Score &p); //拷贝构造函数
private:
int mid_exam;
int fin_exam;
}
Score::Score(const Score &p)
{
mid_exam = p.mid_exam;
fin_exam = p.fin_exam;
}
自动调用拷贝构造函数的三种情形:
1.当用一个已有对象给另一个同类的对象进行初始化时;
2.当一个函数的参数是一个类的对象,调用函数形参和实参结合时;
3.当一个函数的返回值是一个类的对象,函数执行完成返回调用者时。
浅拷贝:
当需要拷贝的对象有其他资源时(比如动态内存空间,文件…),浅拷贝是无法实现拷贝的需求,此时就需要深拷贝的帮助!
深拷贝:
class Person
{
char* name;
public:
Person(const Person& p);
}
Person::Person(const Person& p)
{
name = new char[strlen(p.name)+1];
strcpy(name,p.name);
}
总结:
在C++中,拷贝构造函数用于一个对象初始化另一个对象。如果我们没有定义拷贝构造函数,则系统会自动创建一个默认的拷贝构造函数,用于浅拷贝,即简单的拷贝对象。如果原始对象拥有动态内存,此时拷贝对象就会被赋值为原始对象的内存地址,这样就意味着两个对象指向了同一块内存空间。那么当析构函数被调用时,动态内存会被释放掉。但这就会导致内存的被释放两次,出现异常报错!为了解决这个问题,我们会自己创建一个构造函数,动态申请内存,将原始对象的内容完整的复制过来,这个过程就是深拷贝!
68.深拷贝与浅拷贝的区别
当类中有指针成员时,浅拷贝只将指针中保存的地址复制给新的对象,实际上还是指的同一块内存,当析构函数对两个对象进行内存释放时会出现double free异常(释放第一个对象时,已将内存删除,第二个对象释放时还是找的原有地址,但内存已不存在)。
深拷贝拷贝时,先开辟一个新的与原对象动态空间大小相同的空间,将原对象指针中指向内存的数据进行拷贝,新对象指针存储新内存的地址,原对象指针存储原内存的地址,所以释放时互不干扰。
析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,析构函数是在对象自动销毁时被系统调用的函数,作用是释放对象中开辟的内存空间,防止内存泄漏。
静态数据成员:
在一个类中,若将一个数据成员说明为static,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。
定义静态数据成员的格式如下:static 数据类型 数据成员名;
静态成员函数:
在类定义中,前面有static修饰的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:
static 返回类型 静态成员函数名(参数表);
友元函数:
//一个函数同时定义为两个类的友元函数
#include
#include
using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
friend void show(Score &sc, Student &st);
};
class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void show(Score &sc, Student &st);
};
void show(Score &sc, Student &st) {
cout << "姓名:" << st.name << " 学号:" << st.number << endl;
cout << "期中成绩:" << sc.mid_exam << " 期末成绩:" << sc.fin_exam << endl;
}
int main() {
Score sc(89, 99);
Student st("白", 12467);
show(sc, st);
return 0;
}
#include
#include
using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
void show(Score &sc);
};
class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void Student::show(Score &sc);
};
void Student::show(Score &sc) {
cout << "姓名:" << name << " 学号:" << number << endl;
cout << "期中成绩:" << sc.mid_exam << " 期末成绩:" << sc.fin_exam << endl;
}
int main() {
Score sc(89, 99);
Student st("白", 12467);
st.show(sc);
return 0;
}
友元类:
可以将一个类声明为另一个类的友元
class Y{
···
};
class X{
friend Y; //声明类Y为类X的友元类
};
常对象:
如果在说明对象时用const修饰,则被说明的对象为常对象。
常对象中的数据成员为常量且必须要有初值。
const Date date(2021, 5, 31);
常对象成员:
class Date{
private:
const int year;
const int month;
const int day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
};
class Date{
private:
int year;
int month;
int day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d){
}
void showDate();
void showDate() const;
};
void Date::showDate() {
//···
}
void Date::showDate() const {
//···
}
说明:
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
#include
#include
using namespace std;
class Base{
protected:
int a;
public:
Base(){
a = 5;
cout << "Base a = " << a << endl;
}
};
class Base1: public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2: public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
class Derived: public Base1, public Base2{
public:
Derived() {
cout << "Base1::a = " << Base1::a << endl;
cout << "Base2::a = " << Base2::a << endl;
}
};
int main() {
Derived obj;
return 0;
}
//运行结果
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25
虚基类的声明:
不难理解,如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字virtual将base声明为虚基类。
修改代码:
class Base1:virtual public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2:virtual public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
//运行结果:
Base a = 5
Base1 a = 15
Base2 a = 35
Base1::a = 35
Base2::a = 35
多态目的:实现逻辑传递。
实现的方法:子类继承父类,重写父类中的虚函数。父类指针指向子类对象时,可以通过父类的指针调用子类中重写的函数。
多态存在的3个条件
1)有继承
2)有重写(虚函数)
3)父类指针或引用指向子类对象
如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:
如果在主函数中定义一个基类的对象指针,用new运算符建立一个派生类的匿名对象,并将匿名对象的地址赋值给这个对象指针,当用delete运算符撤销匿名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。
Base *p;
p = new Derived;
delete p;
-----------------
输出:调用基类Base的析构函数
原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。
#include
#include
using namespace std;
class Base{
public:
virtual ~Base() {
cout << "调用基类Base的析构函数..." << endl;
}
};
class Derived: public Base{
public:
~Derived() {
cout << "调用派生类Derived的析构函数..." << endl;
}
};
int main() {
Base *p;
p = new Derived;
delete p;
return 0;
}
-----------------
调用派生类Derived的析构函数...
调用基类Base的析构函数...
运算符重载为友元函数:
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。
例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+5和5+c这两个表达式都能解释得通。
将+重载为 Complex 类的成员函数能解释c+5,但是无法解释5+c。要让5+c有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:
class Complex
{
double real, imag;
public:
Complex(double r, double i):real(r), imag(i){};
Complex operator + (double r);
friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
return Complex (c.real+r, c.imag);
}
重载++和–(自增和自减运算符)
自增运算符++、自减运算符–都可以被重载,但是它们有前置、后置之分。
以++为例,假设 obj 是一个 CDemo 类的对象,++obj和obj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:
CDemo & CDemo::operator ++ ()
{
//…
return * this;
}
那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。
为了解决这个问题,C++ 规定,在重载++或–时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或–前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:
class CDemo {
private:
int n;
public:
CDemo(int i=0):n(i) { }
CDemo & operator++(); //用于前置形式
CDemo operator++( int ); //用于后置形式
friend CDemo & operator--(CDemo & );
friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
n ++;
return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
CDemo tmp(*this); //记录修改前的对象
n++;
return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
d.n--;
return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
CDemo tmp(d);
d.n --;
return tmp;
}
在 C++ 中进行运算符重载时,有以下问题需要注意:
函数模板:
所谓函数模板,实际上是建立一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。在调用函数时,系统会根据实参的类型(模板实参)来取代模板中的虚拟类型,从而实现不同函数的功能。
#include
using namespace std;
template <typename T>
T Max(T *array, int size = 0) {
T max = array[0];
for (int i = 1 ; i < size; i++) {
if (array[i] > max) max = array[i];
}
return max;
}
int main() {
int array_int[] = {783, 78, 234, 34, 90, 1};
double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
int imax = Max(array_int, 6);
double dmax = Max(array_double, 6);
cout << "整型数组的最大值是:" << imax << endl;
cout << "双精度型数组的最大值是:" << dmax << endl;
return 0;
}
实际上,template是一个声明模板的关键字,它表示声明一个模板。类型参数(通常用C++标识符表示,如T、type等)实际上是一个虚拟的类型名,使用前并未指定它是哪一种具体的类型,但使用函数模板时,必须将类型实例化。类型参数前需加关键字typename或class,typename和class的作用相同,都是表示一个虚拟的类型名(即类型参数)。
注意:
类模板:
所谓类模板,实际上就是建立一个通用类,其数据成员、成员函数的返回类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会根据实参的类型来取代类模板中虚拟类型,从而实现不同类的功能。
template <typename T>
class Three{
private:
T x, y, z;
public:
Three(T a, T b, T c) {
x = a; y = b; z = c;
}
T sum() {
return x + y + z;
}
}
类模板中的成员函数也可以在类模板体外定义:
template<typename T>
T Three<T>::sum() {
return x + y + z;
}
程序中常见的错位分为两大类:编译时错误和运行时错误。编译时的错误主要是语法错误,如关键字拼写错误、语句末尾缺分号、括号不匹配等。运行时出现的错误统称为异常,对异常的处理称为异常处理。
C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。
例子:输入三角形的三条边长,求三角形的面积。当输入边的长度小于0时,或者当三条边都大于0时但不能构成三角形时,分别抛出异常,结束程序运行。
#include
#include
using namespace std;
double triangle(double a, double b, double c) {
double s = (a + b + c) / 2;
if (a + b <= c || a + c <= b || b + c <= a) {
throw 1.0; //语句throw抛出double异常
}
return sqrt(s * (s - a) * (s - b) * (s - c));
}
int main() {
double a, b, c;
try {
cout << "请输入三角形的三个边长(a, b, c): " << endl;
cin >> a >> b >> c;
if (a < 0 || b < 0 || c < 0) {
throw 1; //语句throw抛出int异常
}
while (a > 0 && b > 0 && c > 0) {
cout << "a = " << a << " b = " << b << " c = " << c << endl;
cout << "三角形的面积 = " << triangle(a, b, c) << endl;
cin >> a >> b >> c;
if (a <= 0 || b <= 0 || c <= 0) {
throw 1;
}
}
} catch (double) {
cout << "这三条边不能构成三角形..." << endl;
} catch (int) {
cout << "边长小于或等于0..." << endl;
}
return 0;
}
STL Standard Template Library,即标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。 该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。
STL中六大组件:
STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。
STL的组成 | 含义 |
---|---|
容器 | 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 |
算法 | STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。 |
迭代器 | 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数) |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 |
相关头文件:
<iterator> <functional> <vector> <deque>
<list> <queue> <stack> <set>
<map> <algorithm> <numeric> <memory>
<utility>
它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。
两大类:序列容器 关联容器
容器种类 | 功能 |
---|---|
序列容器 | 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 |
排序容器 | 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。 |
哈希容器 | C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。 |
序列容器:
//vector常用操作
v.capacity(); //容器容量
v.size(); //容器大小
v.max_size(); //表示返回vector最大容量
v[int idx]; //访问元素
v.at(int idx); //用法和[]运算符相同
v.push_back(); //尾部插入
v.pop_back(); //尾部删除
v.front(); //获取头部元素
v.back(); //获取尾部元素
v.begin(); //头元素的迭代器
v.end(); //尾部元素的迭代器
v.insert(pos,elem); //pos是vector的插入元素的位置
v.insert(pos, n, elem) //在位置pos上插入n个元素elem
v.insert(pos, begin, end);
v.erase(pos); //移除pos位置上的元素,返回下一个数据的位置
v.erase(begin, end); //移除[begin, end)区间的数据,返回下一个元素的位置
v.reverse(pos1, pos2); //将vector中的pos1~pos2的元素逆序存储
//list容器常用操作
push_back() //在list的末尾添加一个元素
push_front() //在list的头部添加一个元素
pop_back() //删除最后一个元素
pop_front() //删除第一个元素
sort() //给list排序
reverse() //把list的元素倒转
关联容器:
map容器是键-值对的集合,map中的所有元素都是pair,可以使用键作为下标来获取一个值。Map中所有元素都会根据元素的值自动被排序,同时拥有实值value和键值key,pair的第一元素被视为键值,第二元素被视为实值,同时map不允许两个元素有相同的键值。
//map容器常用操作
map<string, int> m;
pair<string, int> p("hehe", 10);
//p.first = key
//p.second = value
m.insert(p) ;
//如果键p.first不在m中,则插入一个值为p.second的新元素;如果该键在m中已存在,那么不进行任何操作。
//用数组方式插入:
m["str"] = 1;
//如果"str"不在m中,则插入一个值为1的新元素;如果该键在m中已存在,则修改值为1;
map<string, int>::iterator iter = m.find("小红");
if(iter != m.end())
cout<<iter->first<<" "<<iter->second<<endl;
else
cout<<"查无此项"<<endl;
//删除操作
//1.通过迭代器指针
iterator erase(iterator it);//通过一个条目对象删除
iterator erase(iterator first,iterator last)//删除一个范围
//2,通过键
m.erase("aaa");
用下标访问map中不存在的元素将导致在map容器中添加一个新的元素,这个元素的键即为该下标值,键所对应的值为空。
迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。
C++中的迭代器是对指针的封装,迭代器提供特定的函数让调用者对指针进行特定的安全的操作。
//vector容器
vector<int> v;
vector<int>::iterator iter;
//begin是容器的头元素 ,end并不是尾元素,而是无效的元素表示容器结束
for(iter = v.begin(); iter != v.end(); iter++)
{
cout<<*iter<<" ";
}
//map容器
map<string, int> m;
for(map<string, int>::iterator iter = m.begin();iter != m.end();iter++)
{
cout<<iter->first<<" "<<iter->second<<endl;
}
//删除
iterator erase();
《C++ Primer》中提到:
“可以用单个形参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。”
这里应该注意的是, “可以用单个形参进行调用” 并不是指构造函数只能有一个形参,而是它可以有多个形参,但那些形参都是有默认实参的。
那么,什么是“隐式转换”呢? 上面这句话也说了,是从构造函数形参类型到该类类型 的一个编译器的自动转换。看一段代码实例:
class CxString
{
public:
char *_pstr;
int _size;
// 没有使用explicit关键字的类声明, 即默认为隐式声明
CxString(int size)
{
_size = size; // string的预设大小
_pstr = malloc(size + 1); // 分配string的内存
memset(_pstr, 0, size + 1);
}
CxString(const char *p)
{
int size = strlen(p);
_pstr = malloc(size + 1); // 分配string的内存
strcpy(_pstr, p); // 复制字符串
_size = strlen(_pstr);
}
// 析构函数这里不讨论, 省略...
};
// 下面是调用:
CxString string1(24); // 这样是OK的, 为CxString预分配24字节的大小的内存
CxString string2 = 10; // 这样是OK的, 为CxString预分配10字节的大小的内存
CxString string3; // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码
string1 = 2; // 这样也是OK的, 为CxString预分配2字节的大小的内存
string2 = 3; // 这样也是OK的, 为CxString预分配3字节的大小的内存
string3 = string1; // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用free释放_pstr内存指针的时候可能会报错, 完整的代码必须重载运算符"=", 并在其中处理内存释放
上面的代码中, “CxString string2 = 10;” 这句为什么是可以的呢? 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 也就是说 “CxString string2 = 10;” 这段代码, 编译器自动将整型转换为CxString类对象, 实际上等同于下面的操作:
CxString string2(10);
或
CxString temp(10);
CxString string2 = temp;
但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 我们把上面的代码修改一下, 如下:
class CxString
{
public:
char *_pstr;
int _size;
// 使用关键字explicit的类声明, 显示转换
explicit CxString(int size)
{
_size = size;
// 代码同上, 省略...
}
CxString(const char *p)
{
// 代码同上, 省略...
}
};
// 下面是调用:
CxString string1(24); // 这样是OK的
CxString string2 = 10; // 这样是不行的, 因为explicit关键字取消了隐式转换
CxString string3; // 这样是不行的, 因为没有默认构造函数
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换
string1 = 2; // 这样也是不行的, 因为取消了隐式转换
string2 = 3; // 这样也是不行的, 因为取消了隐式转换
string3 = string1; // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载
explicit关键字的作用就是防止类构造函数的隐式自动转换.
上面也已经说过了, explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.
但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。
参考文章:
https://baifeng.blog.csdn.net/article/details/117563488?spm=1001.2014.3001.5506