1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
2.构造函数
首先先来看下面的一段代码:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 5, 2);
d1.Print();
return 0;
}
构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次 。
3.对象实例化的时候编译器可以自动调用构造函数.
4.构造函数可以支持重载
那么上面的代码可以改成下面的形式:
class Date
{
public:
Date(int year=2019, int month=2, int day=10)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,5,2);
Date d2;
d1.Print();
d2.Print();
return 0;
}
上面的代码我给了构造函数参数了全缺省,这样我们在调用时就算不穿参数,编译器也会自动调用其构造函数,即在对象实例化时调用了构造函数。
class Date
{
public:
/*Date(int year=2019, int month=2, int day=10)
{
_year = year;
_month = month;
_day = day;
}*/
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,5,2);
Date d2;
d1.Print();
d2.Print();
return 0;
}
就算我们不实现构造函数,编译器也会调用默认的构造函数。
总结:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
我们看了上面的代码可能会产生疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,默认生成的构造函数,对内置类型不做处理,自定义类型会去调用它的默认构造函数。
面对这种情况C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值。
class Date
{
public:
//Date(int year=2019, int month=2, int day=10)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year= 2022;
int _month=4;
int _day=5;
};
int main()
{
//Date d1(2024,5,2);
Date d2;
//d1.Print();
d2.Print();
return 0;
}
来看上面的代码,我们可以给声明的成员变量给缺省值,这样就弥补了我们没有构造函数,编译器自动生产的构造函数却是初始化随机值的问题
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
class Date
{
public:
Date()
{
_year = 2000;
_month = 5;
_day = 2;
}
Date(int year = 2000, int month = 5, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
int main()
{
Date d1;
return 0;
}
答案是不能的,因为会引起编译歧义。虽然这两个函数构成了函数重载,两个构造函数都符合调用条件,但编译器不知道要调用哪一个,故报错了。
2.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
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)
{}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year = 2022;
int _month = 4;
int _day = 5;
};
int main()
{
Date d1(2024,5,2);
//Date d2;
d1.Print();
//d2.Print();
return 0;
}
注意:1. 每个成员变量在初始化列表中 只能出现一次 ( 初始化只能初始化一次 )2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量const 成员变量自定义类型成员 ( 且该类没有默认构造函数时 )
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
,n(1)
{}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
cout << n << endl;
}
private:
int _year = 2022;
int _month = 4;
int _day = 5;
const int n;
};
int main()
{
Date d1(2024,5,2);
//Date d2;
d1.Print();
//d2.Print();
return 0;
}
上面我给了成员变量缺省值,其实:
缺省值的本质就是给初始化列表用的
可以看到我们在初始化时都是先走初始化列表的。
2.2成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
来看下面的代码:
class A
{
public:
A(int a)
:_aa2(a)
,_aa1(_aa2)
{}
void Print()
{
cout << _aa1 << " " << _aa2 << endl;
}
private:
int _aa1;
int _aa2;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
看看代码运行结果是如何呢?
直接看结果:
结果是随机值和1
在这个例子中,A类有两个整型成员变量:_aa1和_aa2。在构造函数中,_aa2被初始化为传入的参数a的值,而_aa1被初始化为_aa2的值。
然而,成员变量的初始化顺序是由它们在类中声明的顺序决定的,而不是它们在初始化列表中出现的顺序。在A类中,_aa1在_aa2之前声明,因此_aa1会先于_aa2初始化。
这意味着当_aa1(_aa2)执行时,_aa2还没有被初始化,所以_aa1的值是未定义的。然后,_aa2被初始化为1.
因此,当调用aa.Print();时,输出的第一个值(_aa1的值)是未定义的,而第二个值(_aa2的值)是1。在实际执行时,未定义的值可能是内存中该位置的任何值,这取决于编译器和运行时环境。
要修正这个问题,应该按照成员变量在类中声明的顺序初始化它们,或者更改成员变量的声明顺序以反映期望的初始化顺序。例如:
将顺序调整好就可以了。
在这个修改后的版本中,_aa1
会先被初始化为1
,然后_aa2
会被初始化为_aa1
的值,即1
。所以Print
函数会输出1 1
。
3.隐式类型转换
来看看下面这段代码:
class A
{
public:
A(int a)
:_a(a)
{}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A aa1(2);
A aa2 = 3;
aa1.Print();
aa2.Print();
return 0;
}
其结果是 2 3
这里aa2直接被赋值了,那么为什么呢?
在C++中,如果一个类的构造函数只需要一个参数(或所有参数除了第一个外都有默认值),那么这个构造函数允许从构造函数参数类型到类类型的隐式转换。
这行代码演示了隐式类型转换。虽然看起来像是将整数3赋值给aa2,实际上C++编译器解释为使用3作为参数调用C类的构造函数来初始化aa2。这是因为C(int a)构造函数允许从int到C的隐式转换。
改初始化通常发生在使用=操作符进行对象初始化的场景中。不同于直接初始化(直接调用构造函数),初始化涉及到源对象到目标对象的潜在类型转换和赋值操作
类型转换:编译器使用3调用A的构造函数创建一个临时的A类型对象。
拷贝构造函数:这个临时对象然后用于初始化aa2。
最后的结果应该是常数3与aa2有一个与aa2相同类型的中间常量,这个常量也会调用构造函数,而当这个这个常量给予aa2时 (类比为:A aa2 = tmp(相当与中间常量))时会进行拷贝构造,最后应该是构造函数加拷贝构造函数都调用才对,但实际上编译器对此连续的构造+拷贝构造会进行优化,最后优化成只有调用构造函数。
所以明白了中间的原理,那么下面的代码可行吗?
class A
{
public:
C(int x)
:_x(x)
{}
private:
int _x;
};
int main()
{
C& aa3 = 2;
return 0;
}
实际上我们知道这其中会有隐式类型转换,那么中间就会有一个只读的临时常量,
引用的基本要求:在C++中,引用必须绑定到一个已经存在的对象上。引用本质上是对象的别名,它不能像指针那样可以更改指向的对象。
引用与临时对象:尽管临时对象(如通过类型转换创建的临时C对象)可以被绑定到const引用上(即const A&),但它们不能直接绑定到非const引用(A&)上。这是为了防止通过非const引用对临时对象进行修改,因为这种修改通常没有意义(临时对象在表达式结束后就销毁了),而且引用的权限被放大了,这是语法所不允许的
正确的用法:如果你的意图是创建一个A类型的临时对象,并将其绑定到引用上,正确的语法应该使用const引用:
const A& aa2 = 3; // 依赖于A(int)构造函数的隐式类型转换
4.explicit关键字
那么如果不想让隐式类型转换发生,我们就需要用 explicit
修饰构造函数,禁止类型转换
单参构造函数,没有使用explicit修饰,具有类型转换作用
C++11及以后版本版本支持多个参数隐式类型转换
class A
{
public:
//explicit A(int a,int b = 2)
A(int a,int b = 2)
:_a(a)
,_b(b)
{
cout << "A(int a)" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
void Print()const
{
cout << _a <<" "<<_b << endl;
}
private:
int _a;
int _b;
};
int main()
{
A x = {1,3};
x.Print();
return 0;
}
想让隐式类型转换发生,可以加上explicit关键字