文章目录
- 一.再谈构造函数
- 初始化对象的两种方式
- 1.函数体内赋值
- 2.初始化列表
- 深入理解初始化列表
- 3.explicit
- 隐式类型转换
- 二.static成员
- 引言
- static成员
- static成员函数
- 练习
- 三.友元
- 引入
- 友元函数
- 友元类
- 四.内部类
- 基本概念
- 练习
- 五 .匿名对象
- 引入
- 匿名对象
- 补充:编译器的优化
一.再谈构造函数
初始化对象的两种方式
我们必须清楚的记住:
1.定义一个类,类里面的成员变量只是一个声明。——图纸
2.用类名定义一个对象,这叫做对象的实例化——造房子
1.函数体内赋值
#include<iostream>
using namespace::std;
class A
{
public:
A(int a = 1, int b = 1, int c = 1)
{
_a = a;
_b = b;
_c = c;
}
private:
int _a;//这里只是声明
int _b;
int _c;
};
int main()
{
A a;
return 0;
}
- 在函数体内部将对象成员变量赋值的方式我们称之为函数体内赋值
- 严格上来说,在对象空间创建的同时给成员变量一个值,这才叫初始化。
- 而函数体内赋值更像是对象创建后再给成员变量值,这叫赋值操作。
- 举我们学过的例子
-
- 引用必须初始化——总得给谁取个外号吧,这个谁总得有吧
-
- auto必须初始化——总得识别一个数才能知道其类型吧,这个数总得有吧
-
- const修饰的变量必须初始化——总得有个确定的数吧,这个数总得有吧,后面再给就不合适了。
-
- 自定义的对象有构造函数但不是默认构造也得初始化 ——总得给其成员一个确定的值吧,构造函数不能在定义之后再调用吧?
- 总结:我们可以理解为这样规定是为了安全性考虑的
为了验证是不是我们想的那样我们可以try一下。
#include<iostream>
using namespace::std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class A
{
public:
A(int a, int b, int c)
{
_a = a;
_b = b;
_c = c;
_d = 1;
int x = 0;
_e = x;//这个引用的例子不太恰当,看一下就行
//下面这三个如果是初始化的话,编译器是不会报错的
}
private:
int _a;
int _b;
int _c;
const int _d;
int& _e;
Date _f;
};
编译一下:
- 因此:构造函数内部进行的成员赋值不是初始化!
- 那对上述如何进行初始化呢?
2.初始化列表
- 初始化列表长什么样子呢?
- 冒号开始后跟成员再加括号——括号里放着初始值
- 接着都是逗号开始后跟成员再加括号
- 逻辑表达式:
- :成员变量(初始值)
- , 成员变量(初始值)
具体代码
class A
{
public:
A(int a, int b, int c,int x)
:_a(a)
,_b(b)
,_c(c)
,_d(1)
,_e(x)//这个例子不太恰当,应该函数参数x应该设置为引用。
,_f(2023,5,14)//此处调用的是构造函数
{
}
private:
int _a;
int _b;
int _c;
const int _d;
int& _e;
Date _f;
};
再次编译一下:
很显然没有报错
- 因此:初始化列表是成员定义的地方
- 注意: 一个成员最多初始化一次,也就是说有的成员是可以不用显式的写在初始化列表,但是有的成员是必须显式的出现在初始化列表中。——总结关键:显式
- 说明:必须显式的出现在初始化列表的成员有:
-
- 引用
-
- const修饰的变量
-
- 有构造函数但不是默认构造函数的自定义类型
- 具体原因上面已经说明。
继续分析C++11引出的一个补丁:
- 编译器生成默认构造函数对内置类型不做处理,但我们可以在声明处给缺省值。
class Date
{
public:
private:
int _year = 1;//这是缺省值,可不是初始化
int _month = 1;
int _day = 1;
};
- 既然是缺省值,那这个缺省值给了谁呢?
- 很显然有了前面的铺垫,我们应该能很容易的想出这个缺省值是给了初始化列表的。
- 但是:这里我们没写初始化列表。
- 因此:即使不写初始化列表,也会走初始化列表,因为初始化列表是成员定义的地方!
- 那如果我们显示写一下初始化列表:
class Date
{
public:
Date()
:_year(2)
,_month(2)
,_day(31)
{
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
- 这时候我们该用哪一个值呢?
- 缺省值,顾名思义就是备胎,人家正谈呢那轮的到备胎啊,所以初始化列表显示赋值就用那个值,没赋值才用缺省值。
- 那么新问题出现了——既然初始化列表可以初始化,那还要函数体有何用呢?
- 其实不然,比如我们写一个栈对象,初始化时,对malloc返回的值要进行检查,那么这个检查的工作谁来做呢?当然是函数体了!
代码:
class Stack
{
public:
Stack(int capacity = 10)
:_a((int*)malloc(sizeof(int)*capacity))
,_top(0)
,_size(capacity)
{
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
private:
int* _a;
int _top;
int _size;
};
- 因此:我们要根据实际情况进行灵活应对这些情况。
深入理解初始化列表
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();
}
- 请问此代码的执行结果是什么?
- 答案: 1和随机值
- why?listen to me carecfully!
看这样的调试我们注意仔细看初始化成员列表的那两行代码,按照我们的想法不是应该先走_a1再走_a2吗?但为啥先走_a2再走_a1?
-
答案其实很简单,走初始化列表的顺序与你写初始化列表的顺序无关,只与类中声明成员变量的顺序有关。
具体声明顺序是什么呢?
-
如何避免这样的理解错误发生呢?
-
也很简单,声明的顺序与写初始化列表的顺序一致即可。
改进代码:
class A
{
public:
A(int a)
: _a2(a)
, _a1(_a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
再次执行:
3.explicit
隐式类型转换
class Date
{
public:
Date(int year = 1)
{
year = 1;
}
//拷贝构造
Date(Date& B)
{
_year = B._year;
}
void Print() const//D类型为const因此this指针也必须为const类型的
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date A;//调用默认的拷贝构造
Date B = A;//调用的是拷贝构造,本质上为Date B(A);
Date C = 1;//隐式类型转换,先将1转换为一个Date类型的临时变量,再将临时变量拷贝到C中
//但是在编译器运行时,为了提升效率会将这两步合二为一调用构造函数。
//需要特别注意的是——这里的1发生隐式类型转换的条件为,得有合适的拷贝构造。
const Date& D = 1;//这里也发生隐式类型转换,将1转换为Date类型的临时变量,
//不要忘记这里的临时对象具有常属性!
//因此要加上const,但这里只是个临时变量,那是否会销毁呢?
//打印一下
D.Print();
return 0;
}
程序运行结果:
-
很显然这里的临时变量销毁了,因此这里的引用是十分危险的,因为访问了一块被销毁的空间。
-
有没有方法不让隐式类型转换发生呢?
-
答案显然是有的,那就是在构造函数的前面加上explicit
class Date
{
public:
explicit Date(int year = 1)
{
year = 1;
}
//拷贝构造
Date(Date& B)
{
_year = B._year;
}
void Print() const
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date A;
Date B = A;
Date C = 1;
const Date& D = 1;
D.Print();
return 0;
}
编译一下:
- 很显然,编译器在编译的阶段就阻止了我们发生隐式类型转换。
二.static成员
引言
为了引出static成员我提出一个问题,如何实现一个确定我们当前正在使用的对象的个数的程序?
- 原理:
- 每次生成一个对象,都会调用其构造函数,那么我们只需要设置一个变量初始化为0,在每次调用构造函数时加1即可。那这个变量是什么类型才能满足我们的需求呢?
- 一般我们都会设置一个全局变量,这样哪都能访问。
实现代码:
int count_obj = 0;
class Date
{
public:
Date(int year = 1)
{
_year = 1;
count_obj++;
}
Date(Date& B)//拷贝构造函数本质上是构造函数的重载形式
{
_year = B._year;
count_obj++;//我们也要在拷贝构造里面加加
}
~Date()
{
count_obj--;
}
private:
int _year;
};
void func(Date C)//这里只会调用拷贝构造,也会调用析构函数,不会调用构造函数。
{
Date D;
cout << __LINE__ << ":" << count_obj << endl;
}
//这里的C和D都会销毁,你还记得销毁的顺序吗?
//与构造函数的调用顺序相反,这里的D先销毁然后C再销毁
int main()
{
Date A;
cout << __LINE__ << ":" << count_obj << endl;
Date B;
cout << __LINE__ << ":" << count_obj << endl;
func(B);
cout << __LINE__ << ":" << count_obj << endl;
return 0;
}
- 补充: __LINE__——显示当前的行数
运行一下:
- 还用上面的类,加上下面的代码。
void func()
{
static Date D;
cout << __LINE__ << ":" << count_obj << endl;
}
int main()
{
Date A;
cout << __LINE__ << ":" << count_obj << endl;
Date B;
cout << __LINE__ << ":" << count_obj << endl;
func();
func();
cout << __LINE__ << ":" << count_obj << endl;
return 0;
}
执行一下:
static成员
- 到这里问题就结束了吗?显然没有,接着深入分析,既然我们创建了此全局变量,那么别人是不是想用就用了,这样代码就不是很安全了,那如何将此全局变量封装成只有该类能用的全局变量呢?那就轮到static出手了。
- 如何操作?且听我娓娓道来。
- 我们首先应该了解static的定义:
- 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
- 用static修饰的成员函数,称之为静态成员函数。
- 静态的成员变量一定要在类外进行初始化
那如何操作呢?按照定义我们应该在类里面声明一个static成员,然后再类外初始化。
具体代码:
class Date
{
public:
Date(int year = 1)
{
_year = 1;
_count_obj++;
}
~Date()
{
_count_obj--;
}
private:
int _year;
static int _count_obj;//这里只是声明而不是定义。
//static修饰的变量存储在静态区,那么不随对象示例化的存在而存在,也不随对象销毁而销毁。
//因此:static修饰的变量属于所有该类的对象。属于共享成员。
};
int Date::_count_obj = 0;//这是用域作用限定符进行初始化。
int main()
{
Date A;
return 0;
}
static成员函数
- 这里如果我们想打印一下_count_obj呢?
- 由于这里是私有成员,那么就只能通过成员函数就能访问。
实现代码:
class Date
{
public:
Date(int year = 1)
{
_year = 1;
_count_obj++;
}
~Date()
{
_count_obj--;
}
void Print_Using_Obj()
{
cout<<__LINE__<<":" << _count_obj << endl;
}
private:
int _year;
static int _count_obj;
};
int Date::_count_obj = 0;
- 但是这里又有一个问题就是这个函数必须通过对象才能访问,那有没有什么方法不用通过对象访问,而是通过类域直接访问呢?
- 当然有了设置一个static函数!
class Date
{
public:
Date(int year = 1)
{
_year = 1;
_count_obj++;
}
~Date()
{
_count_obj--;
}
//说明这里只是多了一种访问方式——既可以通过对象访问,也可以通过类域进行访问。
//但要注意一点这里的函数是没有this指针的,也就是说不能用const限定符进行修饰。
static void Print_Using_Obj()
{
cout<<__LINE__<<":" << _count_obj << endl;
}
private:
int _year;
static int _count_obj;
};
int Date::_count_obj = 0;
int main()
{
Date::Print_Using_Obj();
return 0;
}
总结:
- 一. static 成员
-
- static修饰的成员必须在类外,用域作用限定符进行初始化,定义不用加static
-
- static修饰的成员是所有类对象共享的,并不属于某一个特定的对象。
-
- static修饰的成员若为私有,只能通过成员函数进行访问。
-
- static成员是不会走初始化列表的!因此不能给static缺省值.
- 二. static修饰的成员函数
-
- 没有this指针,间接说明不能用const进行修饰。也不能访问成员变量(不加static)。
-
- 既可以通过类域进行访问,也可以通过对象进行访问,其本质都是突破类域进行访问!
练习
- 求1+2+3+…+n
- 条件:
- 1.不能使用乘除法、
- 2.for、while、if、else、switch、case等关键字
- 3.条件判断语句(A?B:C)。
- 此外我们再加几条:
-
- 不得使用异或
-
- 不得使用递归
那我们可以用什么呢?当然是static了!
- 不得使用递归
具体如何实现呢?
class Sum
{
public:
Sum()
{
_n+=_i;
_i++;
//每次创建一次_n加上i同时_i加等上1这样就达到了累加求和的效果。
}
static int GetSum()
{
return _n;
}
private:
static int _n;
static int _i;
};
int Sum::_n = 0;
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n)
{
Sum sum[n];//这里是变长数组
//如果编译器不支持变长数组可以这样写
return Sum::GetSum();//返回的是_n
}
private:
};
三.友元
引入
- 当我们实现日期类的输入和输出时,如果放在类里面进行实现,那要怎么实现呢?
实现代码:
class Date
{
public:
ostream& operator<<(ostream& out)
{
out << _year << "年" <<_month << "月" << _day << "日" << endl;
return out;
}
istream& operator>>(istream& in)//——>(Date * const this, istream &in)
{
cin >> _year >> _month >> _day;
return in;
}
private:
int _year = 1949;
int _month = 10;
int _day = 1;
};
- 但是这样要如何使用呢?
- 由于:这里本质上是两个参数——this指针和输出/输入,并且先后顺序不能变
int main()
{
Date A;
A >> cin;//跟我们一般使用的cin<<A;相反
A << cout << endl;//跟我们一般使用的cout<<A;也相反。
return 0;
}
看这样调用是不是很别扭?
- 如何改成我们想要的形式呢?
- 这就引出了友元函数。
友元函数
- 概念:
- 友元函数可以直接访问类的私有成员。
- 它是定义在类外部的普通函数,不属于任何类,
- 需要在类的内部声明,声明时需要加friend关键字。
接着改进我们上面的代码:
class Date
{
friend ostream& operator<<(ostream& out, const Date& date);
friend istream& operator>>(istream& in, Date& date);
public:
private:
int _year = 1949;
int _month = 10;
int _day = 1;
};
ostream& operator<<(ostream& out, const Date& date)
//打印时不需要对类里面的成员进行修改,因此对象要用const进行修饰
{
out << date._year << "年" << date._month << "月" << date._day << "日";
//直接访问类里面的成员,不管是私有还是公有。
return out;
}
istream& operator>>(istream& in, Date& date)
//输入要对对象进行修改,因此不加const
{
cin >> date._year >> date._month >> date._day;
return in;
}
int main()
{
Date A;
cin >> A;
cout << A << endl;;
return 0;
}
- 这样就舒服很多了。
说明:
- 友元函数不是成员函数,因此不受类访问限定符(pubilc,private,protect)的限制。
- 友元函数没有this指针,因此不能用const进行修饰
- 多个类可公用一个友元函数,因此提高了类的耦合度——降低了类的独立性。
- 友元函数就是普通的函数,因此跟一般的函数调用无区别。
友元类
- 友元类是指一个类所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,也就是说我认识你,你不认识我,转换到友元类中就是友元类可以访问另一类的所有成员,但另一个类不一定访问友元类的所有成员。
- 友元关系不能传递,也就是我认识你,你认识他,但我不认识他。转换到友元类,类1是类2的友元,类2是类3的友元,则类1不是类3的友元。
- 这几个点很重要!!!
- 友元类涉及到定义与声明的问题。
- 一个故事让你彻底理解友元类!
为了了解清楚我们分析一段错误的友元类写法
class Date;
class Time
{
public:
void Print(Date A)
{
cout << A._year << endl;
}
private:
int _hours = 1;
int _minutes = 1;
int _seconds = 1;
};
class Date
{
public:
friend class Time;
private:
int _year = 1949;
int _month = 10;
int _day = 1;
};
- 那正确的应该怎么写呢?
class Time;
class Date
{
public:
friend class Time;
private:
int _year = 1949;
int _month = 10;
int _day = 1;
};
class Time
{
public:
void Print(Date A)
{
cout << A._year << endl;
}
private:
int _hours = 1;
int _minutes = 1;
int _seconds = 1;
};
int main()
{
Date A;
Time B;
B.Print(A);
return 0;
}
四.内部类
基本概念
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
- 注意:
-
- 内部类不属于外部类。
-
- 外部类对内部类没有任何优越的访问权限。
-
- 内部类天然就是外部类的友元类(规定)。内部类——友元类是单向的。
具体代码:
class Date
{
public:
class Time
{
public:
void Print(Date A)
{
cout << A._year << endl;
}
private:
int _hours = 1;
int _minutes = 1;
int _seconds = 1;
};
private:
int _year = 1949;
int _month = 10;
static int _day;
};
int Date::_day = 1;
int main()
{
Date A;
Date::Time B;
B.Print(A);
return 0;
}
- 外部类与内部类唯一的关系,恐怕就是在定义一个内部类的时候了,因为在类域里面所以要通过域作用限定符进行对内部类对象的实例化。
练习
- 以下代码的输出结果为?
class Date
{
public:
class Time
{
public:
void Print(Date A)
{
cout << A._year << endl;
}
private:
int _hours = 1;
int _minutes = 1;
int _seconds = 1;
};
private:
int _year = 1949;
int _month = 10;
static int _day;
};
int Date::_day = 1;
int main()
{
cout << sizeof(Date) << endl;
return 0;
}
执行一下:
- 观众们答对了吗?
- 回答错的别急,下面我们就细讲为什么?
-
前提:sizeof——求的是类实例化对象的大小
-
第一:内部类不属于外部类,因此内部类的空间不属于外部类。
-
第二:static 修饰的成员不属于类对象,而属于整个类,且存储空间不在类里面,而在静态区。
-
第三:成员函数的空间存储在代码区,也不在类里面,属于类对象共享。
-
因此:只有_year和_month在类里面创建,单独属于每个类对象,所以共8字节。
-
说明:友元的声明只是说明谁是谁的友元,而并不能说明谁属于谁!
五 .匿名对象
引入
为了更好的理解下面的知识,我们复习一下匿名结构体。
- 定义结构体时,不写结构体的名字。
struct
{
int _month;
}stu1;//不写结构体名字,直接用结构体名字创建变量。
class
{
public:
int _year = 1;
}s;//匿名类,直接用类名创建对象。
int main()
{
cout << s._year << endl;
cout << stu1._month << endl;
return 0;
}
匿名对象
由上面的匿名结构体可知:
- 匿名对象——对象创建时不写名字。
代码:
class Date
{
public:
Date(int year)
:_year(1949)
,_month(10)
,_day (1)
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A(1949);//有对象名,所以叫有名对象
Date(1949);//没有对象名,所以叫匿名对象
return 0;
}
说明:匿名对象的格式——类名+(值);
注意:这个值有默认构造可不写
- 那匿名对象和有名对象有什么区别吗?
- 答案肯定是有的,我们调试一波。
- 这样我们不难看出:
- 因为在走过Date(1949)之后,调用了构造函数和析构函数
- 因此:匿名对象的生命周期在其存在的那一行。
- 那不少观众就要问了——既然匿名对象如此短命,那么匿名对象有啥用呢?
- 肯定是有用的,只是我们的见过的东西太少了以至于不太了解这个东西的用法。
- 简单说明一个用法:
当我们想用对象的一个函数,而又不想额外的开辟一个对象——之后这个对象就不用了,进行调用。那我们要怎么办呢?——用一个随用随弃的对象(匿名对象)
class Date
{
public:
Date(int year)
:_year(1949)
,_month(10)
,_day (1)
{
cout << "Date()" << endl;
}
void Func(int n)
{
//实现的功能
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A(1949);
A.Func(50);
Date(1949).Func(50);//像这样进行调用,调用完就释放了,这样就节省了一部分的开销。
return 0;
}
- 既然匿名对象的周期很短,那能不能延长它的生命周期呢?
- 答案是:有的,吃一颗伸腿瞪眼丸——const +引用
class Date
{
public:
Date(int year)
:_year(1949)
,_month(10)
,_day (1)
{
cout << "Date()" << endl;
}
void Print()const
{
cout << _year << "年" << _month << "月" << _day << "天" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date& date = Date(1949);
date.Print();
//这里所传的this指针是const Date* const 类型的,因此Print函数需要用const修饰一下。
return 0;
}
来调试一下:
- 我们可以看到,在匿名对象进行引用加const之后,在之后析构函数没有调用。
- 在main函数结束之后才进行调用析构。
- 结论1:加了const与引用匿名对象的生命周期延长为当前函数局部域。
- 注意:续命之后的匿名对象只能调用const修饰的成员函数
- 结论2: 之所以加上const是因为匿名对象具有常属性。
补充:编译器的优化
- 编译器通常会将连续的构造进行优化。
- 注意:
-
- 连续指的是在同一条语句
-
- 这里的构造指的是——拷贝构造和构造
例1代码:
class Date
{
public:
Date(int year = 1949)
:_year(year)
{
cout << "Date()" << endl;
}
Date(const Date& B)
{
_year = B._year;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
Date Func()
{
Date A;
return A;
}
void Func1(Date A)
{
}
int main()
{
Date A = Func();
//这里是一个拷贝构造+拷贝构造优化为拷贝构造
return 0;
}
执行结果:
图解:
- 说明:以下代码沿用上面的类和函数。
例2代码:
int main()
{
Func1(Date());
return 0;
}
执行结果:
图解:
例3代码:
int main()
{
Date D = 1;
return 0;
}
执行结果:
图解:
int main()
{
Date D = 1;
return 0;
}
例4:
int main()
{
Func1(1);
return 0;
}
运行结果:
图解:
例5:
int main()
{
Date A(2020);
Func1(A);
return 0;
}
执行结果:
图解:
- 总结:常属性
- 匿名对象具有常属性
- 返回值具有常属性
- 隐式类型转换的结果具有常属性