日期类的实现,综合运用前述知识。
1)、为什么要使用初始化列表?
要回答这个问题,首先我们要知道类中构造函数为成员变量赋值的两种方式:
在函数体中赋值:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
1、虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2、对日期类对象,在构造函数内赋值或使用列表初始化相差不大,但有些类的成员就必须使用初始化列表进行初始化。
使用初始化列表赋值
2)、初始化列表基本介绍
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
:_year(year) //初始化列表
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
注意事项:1、初始化列表是成员定义的地方。
2)、自定义类型成员必须在初始化列表初始化
对自定义类型成员,若没有默认构造函数,则其初始化需要调用初始化列表。若有默认构造函数,即使在构造函数内部初始化,仍旧会先被初始化列表初始化一次。(即使我们没有手动写初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。)
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
private:
int _year;
Time _t;
};
2)、引用、const类型成员必须在初始化列表初始化
1、引用必须在定义时初始化(C++入门篇,引用的特性)。
2、const成员必须在定义的地方初始化,因为只有一次初始化机会(基于const定义的变量是不能被修改的),对于const成员,其初始化是在构造函数中的初始化列表出。
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)//X加&则是实参y的引用,若不加&则,x的改变不会影响y。
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)//ref是x的引用
{
_ref++;
}
private:
int _year ;
Time _t;
const int _N;//const成员,必须在初始化列表初始化
int& _ref;
};
int main()
{
int y = 0;
Date d(2022, 1, y);
return 0;
}
此处传递y变量给x,若x处的参数不加&,则x只是y的一个临时拷贝,_ref自增能影响x变量但不能影响y变量。
加了&后,_ref的改变能影响y的改变。
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
3)、C++11为构造函数打下的补丁
C++11在默认构造函数中为内置类型打下的补丁,即私有成员声明处的缺省值,实际上它是作用于初始化列表的。
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
private:
// 声明
int _year = 0; // C++11 缺省值-- 初始化时没有显示给值就会用这个缺省值
Time _t;
const int _N;
int& _ref;
};
4)、为什么不统一使用初始化列表?
1、语法设计的循序渐进性。
2、有些内容还是需要在函数体内初始化才方便。
class A
{
public:
A(int N)
:_a((int*)malloc(sizeof(int)* N))//哪怕我们在初始化列表初始化
, _N(N)
{
if (_a == NULL)//在函数体内还需要检查
{
perror("malloc fail");
}
memset(_a, 0, sizeof(int) * N);
}
// 倒不如直接在函数体内完成
A(int N)
:_N(N)
{
_a = (int*)malloc(sizeof(int) * N);
if (_a == NULL)
{
perror("malloc fail");
}
memset(_a, 0, sizeof(int) * N);
}
private:
// 声明
int* _a;
int _N;
};
演示代码:
问题,以下代码输出结果如何?
class A
{
public:
A(int a)
// 成员变量定义
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
// 成员变量声明
int _a2;
int _a1;
};
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
int main() {
// 对象定义
A aa1(1);
aa1.Print();
A aa2(2);
}
结论:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
ps:对象有很多对象,成员列表是针对这些每个对象的单独定义。
1)、引入:单参数的构造函数支持隐式类型转换两种传值方式
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year):"<<_year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
int main() {
// 直接调用构造
Date d1(2021);
// 隐式类型转换:
//构造 + 拷贝构造 -> 编译器优化 = 直接调用构造
Date d2 = 2022;
//原本2022先构造一个tmp,再用tmp拷贝构造d2
//但经过编译器优化,会直接调用构造
//就如普通变量int类型i转换为double时,中间会进过一个临时变量,其具有常属性(const修饰)
//因此,此处若带有&,则需要加上const:具体请回顾类和对象(一)
const Date& d3 = 2023;
int i = 10;
const double& d = i;
}
关于隐式类型转换的再理解:(类和对象·一)
不是直接将 i 的值给 d,它们中间会产生一个临时变量。如果为d加上引用,此处不能直接转换的原因是临时变量具有常属性,此处d引用的是中间生成的那个临时变量,在d没被const修饰时,属于权限放大。
const Date& d3 = 2023;
int i = 10;
const double& d = i;
2)、如何验证:由此引出explict关键字
上述演示似乎并不能证明编译器对这块进行了优化处理,因为也有可能它原本就是如此,因此,我们为其构造函数加上关键字explict,如下所示:
class Date
{
public:
explicit Date(int year)
//Date(int year)
:_year(year)
{
cout << " Date(int year):"<<_year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
int main() {
// 直接调用构造
Date d1(2021);
// 隐式类型转换:
//构造 + 拷贝构造 -> 编译器优化 = 直接调用构造
Date d2 = 2022;
const Date& d3 = 2023;
int i = 10;
const double& d = i;
}
可看到编译器报错,这是因为explict关键字的作用是:防止类对象发生隐私类型转换。
3)、该语法的意义体现:
C++中有一个string类,我们以其为例子进行构造,如下:
int main()
{
string s1("insert");
string s2 = "insert";
}
string s1(“insert”)
:是一种常规写法
string s2 = "insert";
:这种写法也是可行的,且因与内置类型相似,在理解角度相对容易。能支持s2
的写法原因就在于单参数的隐式类型转换。
有一个问题是:明明可以用s1
,为什么要多弄一个s2
的写法?
如下述代码,假设我们有一个函数,其参数是类类型,此时我们该如何传参?
void func(const string& s)
{}
方法一:以string类先构造一个具体的对象,为其赋相应值,再将该对象作为参数传入。
string str("abcde");
func(str);
方法二:直接将常量字符串传入。这里就涉及到一个问题,为什么可以这样写?原因正是在于存在隐式类型转换。
func("abcde");
但这样子的写法需要注意函数参数类型,由于“abcde”属于常量字符串,具有常属性,在引用做参数时,需要加const修饰,即const string& s
,这个条件下才能正常传参。
void func1(string& s)
{}
void func2(string s)
{}
int main()
{
func1("abcde");//error
func2("abcde");//right
return 0;
}
string& s
,若不加const修饰直接传参,属于权限放大,故而会报错。
string s
,至于这种情况,其属于传值传参,非指针非引用,不存在权限问题。
1)、问题引入1:传值传参&&传引用传参,下列分别用了几次构造,几次拷贝构造?
代码一:
void f1(W w)//传值传参
{ }
int main()
{
W w1;
cout << endl;
f1(w1);//传值传参,会产生拷贝构造:一次构造+一次拷贝构造
cout << endl;
return 0;
}
代码二:
void f2(const W& w)//传引用传参
{}
int main()
{
W w1;
cout << endl;
f2(w1);//一次构造:因此建议加上const、&传引用传参
cout << endl<<endl;
return 0;
}
代码三:
void f1(W w)//传值传参
{}
int main()
{
W w1;
cout << endl;
f1(W()); // 匿名对象,本来构造+拷贝构造-->编译器的优化-->直接构造
//结论:连续一个表达式步骤中,连续构造一般都会优化 --> 合二为一
return 0;
}
2)、问题引入2:传值返回和传引用返回,下列分别用了几次构造,几次拷贝构造?
代码一:
W f3()//传值返回
{
W ret;//构造
return ret;//拷贝构造
}
int main()
{
f3();
return 0;
}
代码二:
W f3()//传值返回
{
W ret;//构造
return ret;//拷贝构造
}
int main()
{
//f3();//一次构造。一次拷贝构造。
W w1 = f3();//理论上:一次构造,两次拷贝构造。
return 0;
}
3)、例题进阶:
相关类:
class Widget
{
public:
Widget(int x = 0)
{
cout << "Widget()" << endl;
}
Widget(const Widget& w)
{
cout << "Widget(const Widget& w)" << endl;
}
Widget& operator=(const Widget& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~Widget()
{
cout << "~Widget()" << endl;
}
private:
};
问题:几次构造,几次拷贝构造?
例题一:
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
main() {
Widget x;
Widget y = f(x);
//Widget y = f(f(x));
}
例题二:
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
main() {
Widget x;
//Widget y = f(x);
Widget y = f(f(x));
}
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year):" << _year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main() {
// 匿名对象,生命周期只有这一行
Date(2001);
// 有名对象,生命周期在整个作用域
Date d4(2004);
// 匿名对象 一些使用场景
//创建对象调用:
Solution slt;
slt.Sum_Solution(10);
//匿名对象调用:
Solution().Sum_Solution(10);
//若单纯只是为了调用一次对象中的函数,
//后续也不需要使用该对象
//就可以直接调用匿名对象
//而非为了调用该函数而先创建对象
}
1)、为什么要让类有静态成员
比如,我们会遇到这样的情况:实现一个类,计算程序中创建出了多少个类对象。(相关解答见后续11.2)
一般而言,若没有类静态成员,我们会使用全局变量来实现。但全局变量对所有对象都起作用,如果我们想要一个单独对对象起作用的变量,则就需要静态成员。
2)、静态成员是什么?
声明为static的类成员称为类的静态成员.
①、用static修饰的成员变量,称之为静态成员变量;
②、用static修饰的成员函数,称之为静态成员函数。
PS:静态成员变量一定要在类外进行初始化。
静态成员举例:
此处先简单理解静态成员变量的定义,该部分代码相关解答见后续11.2。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;//定义了一个静态成员变量:此处是声明。
};
int A::_scount = 0;//对其的初始化需要在类外进行。
//需要注意此处要带上域作用限定符表明该成员变量出自哪。
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
两个问题:
1、能否在构造函数的初始化列表中为静态成员初始化?(NO)
2、能否在声明处为静态成员赋予缺省值?(NO)
回答:因为静态成员不能在类内初始化,所以在private处进行缺省赋值也是不被允许的。(由初始化列表得知,C++11的该补丁在初始化列表中起作用)
1)、总体介绍:
2)、类的静态成员变量在公有情况下的访问方式?
在公有情况下,在类外访问静态成员,既可以使用类,也可以使用对象。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
//private:将类成员置为公有
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
//此时的访问方式:
cout << a1._scount << endl;
cout << a2._scount << endl;
cout << A::_scount << endl;
}
int main()
{
TestA();
return 0;
}
此处并非指在a1、a2对象中去寻找_scount
。编译器会在全局中找,然后再去指定的类中寻找。
cout << a1._scount << endl;
cout << a2._scount << endl; //对象.静态成员
cout << A::_scount << endl; //类名::静态成员
3)、静态成员变量在类中声明,如果其被设置为私有,如何访问获取?
访问方式一:
1、如示例代码:可以在类中定义一个GetCount
的函数,如此就可以在TestA
(类外)中使用对象去问了。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
int GetCount() //定义一个成员函数
{
return _scount; //返回私有对象值
}
private://类成员私有时
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
cout << a1.GetCount() << endl; //只能使用对象去调用
cout << a2.GetCount() << endl;
cout << a3.GetCount() << endl;
}
int main()
{
TestA();
return 0;
}
注意事项:访问时是用对象去调用。(即这种写法下不能使用类调用该成员函数)
访问方式二:
2、若不用对象去调用,则需要使用静态成员函数。如所示代码。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetCount()
{
//_a = 1; //error
return _scount;
}
private:
int _a;
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
cout << a1.GetCount() << endl;//对象.静态成员
cout << a2.GetCount() << endl;
cout << A::GetCount() << endl;//类名::静态成员
}
int main()
{
TestA();
return 0;
}
注意事项:
①、由于静态成员函数没有this指针,因此其不能访问非静态成员,只能调用静态成员
②、既然是静态成员函数,说明其写在类中。
4)、面试题:实现一个类,计算程序中创建出了多少个类对象?
class A
{
public:
A() { ++_scount; }//调用构造函数,统计变量自增一次
A(const A& t) { ++_scount; }//调用拷贝构造函数,统计变量自增一次
~A() { --_scount; }//调用析构函数,统计变量自减一次
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}
题源
解决方案:使用变长数组
整体思路: 实际上是借助了变长数组。
对象实例化时会调用构造函数,假若我们定义一个以类为类型的数组,其长度根据我们需要求和的最大数值而定,那么我们就能得到1~100需要求和的数。此时再将其累加记录结果即可。
//定义全局变量
int i=1;
int sum=0;
class Sum
{
public:
Sum()//Sum类的默认构造函数:无参数情况
{
sum+=i;//sum是用于累加这些数值的。
++i;//i是用于统计第几次对象实例化,以模拟实现1~100数值
}
};
class Solution {
public:
//此处将需要写的接口放到了类里
//Sum_Solution成员函数中的变量n即输入值
int Sum_Solution(int n) {
Sum a[n];//使用了变长数组,即数组元素有n个(n是变量),数组类型为Sum类(是一个类)。
//将Sum作为类,当其实例化时,每次都会自动调用对应的构造函数。
//实例化了n个Sum类的对象,构造函数在此处调用了n次。
return sum;//sum是一个全局变量。
}
};
使用全局的i、sum,容易被修改,因此相对不太提倡。因此此处使用了静态成员。
class Sum
{
public:
Sum()//Sum类的默认构造函数:无参数情况
{
_sum+=_i;
++_i;
}
static int GetSum()
{
return _sum;//由于将sum变量设置为了类的成员
//此处变为私有,要访问sum值,可在此处设置一个静态的成员函数
//PS:若不使用静态的成员函数,则调用sum时还需要创建一个对象
//从设计的角度使用静态成员函数比较方便。
}
private:
static int _sum;//静态成员变量的声明
static int _i;
};
//静态成员变量的定义和初始化
int Sum:: _sum=0;
int Sum::_i=1;
class Solution {
public:
//此处将需要写的接口放到了类里
//Sum_Solution成员函数中的变量n即输入值
int Sum_Solution(int n) {
Sum a[n];//使用了变长数组,即数组元素有n个(n是变量),数组类型为Sum类(是一个类)。
//将Sum作为类,当其实例化时,每次都会自动调用对应的构造函数。
//实例化了n个Sum类的对象,构造函数在此处调用了n次。
return Sum::GetSum();
}
};
class Solution {
class Sum//内部类
{
public:
Sum()
{
_sum += _i;
++_i;
}
};
public:
int Sum_Solution(int n) {
Sum a[n];
return _sum;
}
private:
static int _sum;//静态成员变量的声明
static int _i;
};
//静态成员变量的定义和初始化
int Solution::_sum = 0;
int Solution::_i = 1;
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so; //此处使用了传值返回。
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{}
private:
int _x = 0;
int _y = 0;
};
int main()
{
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
故而我们采取了使用友元函数的方式来解决。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
//声明:类中,使用关键字friend进行声明
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;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
1、友元函数不能用const修饰:
const修饰this指针指向的内容,友元函数作为全局函数没有this指针。
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、友元关系是单向的,不具有交换性。
比如示例Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能传递
如果C是B的友元,B是A的友元,则不能说明C时A的友元。
3、友元关系不能继承,在继承位置再介绍。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
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;
};
2)、类中存在内部类时,外部类大小说明:
内部类不属于外部类,因此计算外部类大小时,是不计内部类的。
class A
{
private:
static int k;
int h;
int j;
public:
class B
{
public:
private:
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
2)、内部类存在两点说明:
1、内部类受外部类的类域限制,需要用相应访问限定符
2、内部类天生是外部类的友元,可以访问外部类的私有成员
class A
{
private:
static int k;
int h;
public:
//B定义在A的里面
//1、受A的类域限制,需要用相应访问限定符
//2、B天生是A的友元,即B可访问A的私有成员
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK:A的静态成员可以直接被B访问而不用类名
cout << a.h << endl;//OK:B类可以使用A的私有成员
}
};
};
int A::k = 1;
int main()
{
A::B b;//访问B类受到A的类域限制
b.foo(A());
return 0;
}