深入理解构造函数
构造函数体赋值:
先来看下边一段代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_year = 2023;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(1949, 10, 1);
d.Print();
return 0;
}
编译运行:
可以发现可以正常运行,且构造函数体内的语句都执行了。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对成员变量的初始化,构造函数体中的语句只能将其称为赋值,而不能称作初始化。因为初始化是在定义变量(给变量开辟内存空间)时给变量赋予一个初始值,每个变量只能初始化一次(因为变量只能定义一次),而构造函数体内可以多次赋值。代码中有两个对_year赋值的语句,能够正常编译运行,就印证了上述描述。
初始化列表:
既然成员变量不是在构造函数体内初始化的,那么到底是在哪里初始化的呢?
答案是 成员变量在初始化列表定义并初始化。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
1、初始化列表是成员变量实际定义的地方,再使用时会出现以下几种情况:
(1)没有在初始化列表中显式的写出来,也会在初始化列表中定义,此时内置类型会赋随机值,自定义类型会调用其构造函数。
(2)在初始化列表中写出来了,但是变量后边的括号中没有给定值,此时定义之后变量的值是不确定的(因编译器而异),VS2019下内置类型会被初始化为0,自定义类型会调用其构造函数。
(3)在初始化列表中写出来了,并且变量后边的括号中给定了值,此时内置类型会被定义并初始化为给定的值,自定义类型调用其构造函数。
class A
{
public:
A(int aa = 10)
:_aa(aa)
{
cout << "A(int aa = 10)" << endl;
}
void Print()
{
cout << _aa << endl;
}
private:
int _aa;
};
class B
{
public:
B(int a = 20, int k = 30)
//_a1没有显式的写出来
:_a2() //显式的写出来,但没有给定值
,_a3(1) //显示的写出来,并给了值
,_a4(a) //显示的写出来,并给了值
//_b1没有显式的写出来
,_b2() //显式的写出来,但没有给定值
,_b3(40)//显示的写出来,并给了值
,_b4(k) //显示的写出来,并给了值
{
}
void Print_A()
{
cout << "_a1._aa = ";
_a1.Print();
cout << "_a2._aa = ";
_a2.Print();
cout << "_a3._aa = ";
_a3.Print();
cout << "_a4._aa = ";
_a4.Print();
}
void Print()
{
cout << "_b1 = " << _b1 << endl;
cout << "_b2 = " << _b2 << endl;
cout << "_b3 = " << _b3 << endl;
cout << "_b4 = " << _b4 << endl;
}
private:
A _a1;
A _a2;
A _a3;
A _a4;
int _b1;
int _b2;
int _b3;
int _b4;
};
int main()
{
B b;
b.Print_A();
b.Print();
return 0;
}
运行结果:
2、初始化列表和普通的构造函数体内赋值是可以混用的,且会按顺序先走初始化列表,再走函数体。
有如下代码:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
,_month(2)
{
_year = 2023;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(1949, 10, 1);
d.Print();
return 0;
}
调试代码,监视_year、_month、_day在程序走完初始化列表后的变化:
最终运行结果:
注意:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量;
const成员变量;
自定义类型成员(且该类没有默认构造函数时)。
class A
{
public:
A(int aa)
:_aa(aa)
{
}
private:
int _aa;
};
class B
{
public:
B(int a, int k)
:_a(a)
, _k(k)
, _n(10)
{
}
private:
A _a; //没有默认构造函数
int& _k; //引用
const int _n; //const常量
};
3、尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{
}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
return 0;
}
运行结果:
4、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{
}
void Print()
{
cout << "_a1=" << _a1 << "\n" << "_a2=" << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
运行结果:
5、在声明成员变量时可以给一个值,这个值就是给初始化列表用的。
class Date
{
public:
Date()
{ }
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day;
};
int main()
{
Date d;
d.Print();
return 0;
}
运行结果:
可以发现构造函数体内没有任何语句,初始化列表也没有写出来,但是类对象的成员变量_year、_month并不是随机值,这就说明了类成员变量在初始化列表定义后,还对_year、_month 成员进行了初始化,而这个初始化的值就来源于声明成员变量时给成员变量给定的值,因为_day定义时没有给定值,所以_day是一个随机值。
explicit关键字
构造函数的类型转换:
1、对于单参数的构造函数,在使用时具有类型转换的作用。
class Date
{
public:
//单参数构造函数
Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2022);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
运行结果:
可以发现,明明只定义了一次类对象,但是却调用了两次构造函数,并且还把一个整型的2023赋给了一个类对象。造成这种结果的原因就是上述代码中出现了隐式的类型转换,用整型的2023先构造一个临时的类对象,再把这个临时的类对象赋给d1。
2、有多个参数的构造函数,当创建对象时可以只传一个参数时(全缺省构造函数、半缺省构造函数),也具有类型转换的作用。
class Date
{
public:
//半缺省构造函数
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month = 1, int day = 1)" << endl;
}
//全缺省构造函数
//Date(int year = 2000, int month = 1, int day = 1)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2022);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
运行结果:
结果与单参数的构函数结果相同。
explicit的限制作用:
在构造函数前加上explicit关键字可以禁止构造函数的类型转换作用。
class Date
{
public:
//有explicit修饰的构造函数
explicit Date(int year)
:_year(year)
{
cout << "explicit Date(int year)" << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2022);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
编译时会报错:
重载的 “ = ”运算符的两个操作数都是Date类,并且编译报错无法与整型匹配,说明整型的2023没有进行类型转换,说明explicit禁止了构造函数的类型转换作用。
注意:explicit只能禁止隐式的类型转换,无法禁止强制的类型转换。
class Date
{
public:
//有explicit修饰的构造函数
explicit Date(int year)
:_year(year)
{
cout << "explicit Date(int year)" << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2022);
d1.Print();
//d1=2023;//隐式类型转换
d1 = (Date)2023;//强制类型转换
d1.Print();
return 0;
}
可以正常编译运行:
static成员
概念:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
class A
{
public:
A()
{
++_scount;
}
A(const A & t)
{
++_scount;
}
~A()
{
--_scount;
}
//静态成员函数
static int Get()
{
return _scount;
}
private:
//静态成员变量
static int _scount;
};
//静态成员变量初始化
int A::_scount = 0;
特性:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
3. 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问。
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
//静态成员函数
static int Get()
{
//func();//编译无法通过,静态成员函数没有隐藏的this指针
return _scount;
}
void func()
{
cout << "void func()" << endl;
}
private:
//静态成员变量
static int _scount;
};
//静态成员变量初始化
int A::_scount = 0;
int main()
{
//cout << A::_scount << endl;//编译无法通过,_scount是私有的成员变量
cout << A::Get() << endl;//静态成员函数可以不借助类对象进行访问
A a1;
A a2(a1);
cout << "a1._scount = " << a1.Get() << endl;
cout << "a2._scount = " << a2.Get() << endl;
return 0;
}
运行结果:
a1、a2两个类对象一个调用了构造函数,一个调用了拷贝构造函数,如果静态成员变量时每个类对象独立拥有的,那么a1._scount、a2._scount 的结果应该都是1,但是结果却都是2,这就说明了静态成员变量是所以类对象共有的,不属于某个具体对象。
友元
友元有两种:友元类 和 友元函数。
友元函数:
在类与对象(中)一文中我们尝试实现了一个简单的日期类的一些简单的操作,但是并没有实现日期类的流插入、流提取,现在我们再来实现一下日期类的流插入、流提取(cout、cin)的重载。
class Date
{
public:
Date(int year = 1949, int month = 10, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }
//流插入
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
//流提取
istream& operator>>(istream& _cin)
{
_cin >> _year >> _month >> _day;
return _cin;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cin >> d1;
cout << d1;
return 0;
}
正常来说上述代码应该是可以实现对日期类的流插入、流提取操作,接下来尝试编译运行:
编译无法通过,可能有人会认为是对流插入和流提取的实现有问题,那么再来看下边这段代码:
class Date
{
public:
Date(int year = 1949, int month = 10, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }
//流插入
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
//流提取
istream& operator>>(istream& _cin)
{
_cin >> _year >> _month >> _day;
return _cin;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
//cin >> d1;
//cout << d1;
d1 >> cin;
d1 << cout;
return 0;
}
再次尝试编译运行:
这次却可以正常编译运行,那么问题到底在哪里呢?
首先要知道类成员函数在调用时会有一个隐藏的this指针(静态成员函数没有),并且这个隐藏的this指针会默认占据函数参数的第一个位置,而二元操作符的左操作数在传参时会占据第一各参数的位置。也就是说流插入、流提取函数的参数实际应为:
//流插入
ostream& operator<<(Date* this, ostream& _cout)
{ }
//流提取
istream& operator>>(Date* this, istream& _cin)
{ }
cin>>d1 、 cout<<d1 的写法实际传参是:
cin >> d1;
operator>>(cin, &d1)
cout << d1;
operator<<(cout, &d1)
所以编译时会报错没有匹配的操作数。
而 d1>>cin 、d1<<cout 的写法实际传参为:
d1 >> cin;
operator>>(&d1, cin);
d1 << cout;
operator<<(&d1, cout);
此时才与类中的函数的参数相对应,所以能够正常编译运行。
但是 d1>>cin 、d1<<cout 的写法太过奇怪,并且类成员函数隐藏的this指针又会默认占据第一个参数的位置,那么该怎么办才能实现正常的流插入、流提取功能呢?有人会说直接定义一个全局的函数,但是全局函数又无法访问类中的私有变量。因此到底该怎么办呢?
此时就要用到友元函数了。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
class Date
{
//友元函数声明
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{ }
private:
int _year;
int _month;
int _day;
};
//友元函数定义
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
编译运行:
可以发现此时就可以实现正常的流插入流提取操作了。
注意:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数。
2.友元函数不能用const修饰。
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4.一个函数可以是多个类的友元函数。
5.友元函数的调用与普通函数的调用原理相同。
友元类:
与友元函数类似,友元类就是在A类中用 friend 关键字声明一个B类,B类定义在A类的外边,B类就是A类的友元类。B类中的所有成员函数都是A类的友元函数,都可以访问A类中的隐藏信息(包括私有成员和保护成员)。
class Time
{
//友元类声明
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{ }
private:
//私有成员函数
void Print()
{
cout << _hour << "-" << _minute << "-" << _second << endl;
}
//私有成员变量
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 Set_Time_Date(int hour, int minute, int second)
{
// 访问Date类的私有成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
void Print_Time_Date()
{
// 访问Date类的私有成员函数
_t.Print();
}
private:
int _year;
int _month;
int _day;
Time _t;
};
注意:
1.友元关系是单向的,不具有交换性:
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递:
如果C是B的友元, B是A的友元,则不能说明C是A的友元。
3.友元关系不能继承。
内部类
如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。
内部类是一个独立的类, 它不属于外部类,它是一个受外部类的类域访问限制的普通的类,不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限,但可以在外部类中使用内部类定义成员。
注意:
1.内部类天生就是外部类的友元类。
2.内部类可以不借助 外部类的对象或类名 直接访问外部类的静态成员。
3.sizeof(外部类)和内部类没有任何关系。
class A
{
public:
// 定义内部类B,B天生就是A的友元类
class B
{
public:
void func(A& a)
{
cout << _k << endl;
cout << a._h << endl;
}
};
private:
static int _k;//静态变量
int _h;
};
int A::_k = 1;
int main()
{
A::B b;//B在A的类域内
A a;
b.func(a);
cout << sizeof(A) << endl;
return 0;
}
运行结果:
匿名对象
匿名类即只使用 类名+() 的方法定义匿名对象,匿名对象的特点是不用取名字,但是匿名对象的生命周期只有这一行,到下一行就会自动调用析构函数。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//A a();//错误的定义对象方法,无法与函数声明区分
A();//匿名对象
return 0;
}
调试代码:
可以发现刚运行至return语句时,就已经调用的析构函数。