C++类和对象详解(下篇)
文章目录
- 1:构造函数二刷
- 1.1:构造函数体赋值
- 1.2:初始化列表
- 1.2.1:自定义类型成员变量
- 1.2.2:引用类型成员变量
- 1.2.3:const成员变量
- 1.3:默认构造函数易出错的几个例子
- 1.3.1:一
- 1.3.2:二
- 1.4:初始化顺序
- 1.5:总结
- 1.6:explicit关键字
- 1.6.1:单参构造函数
- 1.6.2:第一个参数无默认值其他参数都有默认值
- 2:static成员
- 2.1:static成员函数
- 3:友元
- 3.1:友元函数
- 3.2:友元类
- 4:内部类
- 4.1:概念
- 4.2:内部类的例题
- 5:匿名对象
- 6:拷贝对象时的VS编译器做出的优化
- 6.1:第一种优化场景
- 6.2:第二种优化场景
- 6.3:第三种优化场景
1:构造函数二刷
1.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:初始化列表
格式:以冒号开始,接着逗号分隔的数据成员列表,成员后跟一个放在括号中的初始化值或者表达式。
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.1:自定义类型成员变量
根据前面的学习,在创建类类型对象的时候,编译器会对内置类型调用构造函数,对自定义类型调用它的默认构造函数。然而这里我们已经显式定义构造函数,编译器将不再生成默认构造函数。
class datetwo
{
public:
datetwo(int x)
:_x(x)
{}
private:
int _x;
};
class date
{
public:
date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
datetwo p;
};
int main()
{
date a;
return 0;
}
定义好一个datetwo类,在date类中声明一个自定义类型成员变量p,但是我们的datetwo类的构造函数使用了初始化列表,如果调试会发现无法编译成功。
因此如果有自定义类型的成员变量,要放在初始化列表中初始化。
1.2.2:引用类型成员变量
在前面的章节中我们学过,引用指向一个实体后就不能再更改,也就是引用只能定义一次,但是如果是构造函数是可以多次初始化赋值,对引用类型的成员变量就会有错误。因此引用也必须放在初始化列表中初始化。
class date
{
public:
date(int year,int month,int day,int a)
:_year(year)
,_month(month)
,_day(day)
,_a(a);
{}
private:
int _year;
int _month;
int _day;
int& _a;
};
1.2.3:const成员变量
const成员变量因为无法更改值只能定义一次,也必须放在初始化列表中。
1.3:默认构造函数易出错的几个例子
1.3.1:一
class datetwo
{
public:
datetwo(int x)
:_x(x)
{}
private:
int _x;
};
class date
{
public:
date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
datetwo p;
};
int main()
{
date a;
return 0;
}
显式定义构造函数后,自定义类型无法调用默认构造函数,出错
1.3.2:二
class datetwo
{
public:
datetwo(int x)
{}
private:
int _x;
};
class date
{
public:
date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
datetwo p;
};
这边没有main函数,也就是没有创建类类型对象,也就是不调用默认构造函数的情况下。
对于datetwo类也没有给初始化列表,但是在date类中有自定义成员变量datetwo p,对于自定义类型首先是会调用初始化列表,这里初始化列表没有给自定义类型初始化。
但是不写初始化列表,也会走一遍初始化列表,因为没有所以就要调用默认构造函数,默认构造函数又显式了,所以这里也会提示没有默认构造函数,出错。
1.4:初始化顺序
初始化的顺序跟声明顺序有关,和在初始化中的先后顺序无关
using namespace std;
class a
{
public:
a(int a)
:_a(a)
,_b(_a)
{}
void print()
{
cout << _a << ' ' << _b << endl;
}
private:
int _b;
int _a;
};
int main()
{
a wjw(1);
wjw.print();
return 0;
}
这是运行结果,因为是先初始化b所以b是随机值,然后是a,a是1。
1.5:总结
必须要放在初始化列表的三种类型变量。
- 自定义类型
- const
- 引用
1.6:explicit关键字
概念:构造函数不仅可以构造和初始化对象,对于单个参数和第一个参数无默认值其他参数都有默认值的构造函数,还具有类型转换的作用,如下;
1.6.1:单参构造函数
class date
{
public:
date(int year)
:_year(year)
{}
date& operator=(const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date a(2008);
a = 2010;
}
2010是一个整形,而我们的赋值运算重载的参数是一个类,这如何呢?
可以看到date类的构造函数只有一个参数,因此编译器会首先将2010这个整形构造为一个无名对象,然后用这个无名对象对a进行赋值。
如图,a对象的year被2010赋值。
因此这种类型转换的含义就是,a = 2010先构造2010对象再拷贝给a,也就是先构造再拷贝。
1.6.2:第一个参数无默认值其他参数都有默认值
class date
{
public:
date(int year,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
date& operator=(const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date a(2008);
a = { 2010,10,10 };
return 0;
}
对于这种情况,就要用{}进行类型转换。
因为上述代码可读性不好,所以用explicit关键字修饰构造函数,将禁止类型转换。
- 全缺省的也可以当做多个参数没有参数。也可以类型转换。
2:static成员
类中如果定义static成员,这个成员属于类,但是这个成员在静态区,这个类类型的对象都共享的。因此构造函数中不需要对static成员初始化。
class date
{
public:
private:
int _year;
int _month;
static int n;//声明
};
//定义
int date::n = 5;
-
定义在外面定义并且使用域作用限定符:: 。
-
定义的时候不需要static,在类中只是声明。
-
作用域受类限制,生命周期是全局的。
class date
{
public:
private:
int _year;
int _month;
static int n;//声明
};
//定义
int date::n = 5;
int main()
{
date a;
cout << a::n << endl;//here
return 0;
}
因为static成员也属于类,所以如果是private的,上述代码也不能访问到n,必须改成public,但是改成public也不太适合使用习惯。
2.1:static成员函数
static成员函数没有this指针,不能访问任何非静态成员
3:友元
3.1:友元函数
在日期类中,我们使用operator运算符重载<<的时候,需要cout是形参第一个,但是成员函数的形参第一个是隐藏的this指针,这个时候我们就需要想到没有this指针的全局成员函数(定义在类外的函数),但是会导致类外无法访问成员,这个时候就需要使用友元函数。
友元函数可以访问类的私有成员变量。
特征如下:
- 在类外任何地方定义,不受类访问限定符限制
- 可以访问类的私有和保护成员,但不是类的成员函数
- 不能用const修饰
- 一个函数可以是多个类的友元函数
- 调用与普通函数调用原理相同
class date
{
public:
friend istream& operator>>(istream& in,date& d);
friend ostream& operator<<(ostream& out, const date& d);
date(int year = 1900,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
istream& operator>>(istream& in,date& d)
{
in >> d._year;
in >> d._month;
in >> d._day;
return in;
}
ostream& operator<<(ostream& out,const date& d)
{
out << d._year <<' ' << d._month <<' ' << d._day;
return out;
}
int main()
{
date d;
cin >> d;
cout << d << endl;
return 0;
}
3.2:友元类
class date
{
public:
/*friend istream& operator>>(istream& in,date& d);
friend ostream& operator<<(ostream& out, const date& d);*/
friend class time;
date(int year = 1900,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
class time
{
public:
time(int hour, int min, int sec)
:_hour(hour)
, _min(min)
, _sec(sec)
{}
void settime(int year, int month, int day)
{
_d._year = year;
_d._month = month;
_d._day = day;
}
private:
int _hour;
int _min;
int _sec;
date _d;
};
- 友元类是定义在一个类外面的,可以访问类的私有成员变量。
- 在上述代码中,time类为date类的友元,所以time类可以访问date类的成员,但是反之不行。
- 友元关系不可继承
- A是B的友元 B是C的友元不能说明A是C的友元,友元关系无法传递。
4:内部类
4.1:概念
- 内部类是外类的友元。
- sizeof(外部类) = 外部类,与内部类大小没关系。
- 可以定义在外部类的public或者proteced或者private都可以。
- 内部类可以直接访问外部类的static成员并且不需要加外部类的对象名。
4.2:内部类的例题
思路:根据之前的学习,创建一个对象就会调用一次构造函数,因此只需要创造n个对象。
class Solution {
public:
class Sum
{
public:
Sum()
{
_sum += _k;
_k++;
}
};
int Sum_Solution(int n)
{
Sum arr[n];
return _sum;
}
static int _k;
static int _sum;
};
int Solution::_k = 1;
int Solution::_sum = 0;
解题思路:在solution类中创建一个sum类,solution类中定义一个k和sum,sum用于保存和,k用来表示每次加的这个数字也就是(1-n),将k和sum设置为全局变量,这样子每个对象都可以共享这个变量,在sum类中定义构造函数,构造函数内容就是sum+=k,k每次+1,然后再到solution类的构造函数中创建一个n个对象的对象数组。
5:匿名对象
-
创建对象的时候不需要对象名
-
生命周期只有一行
class A
{
public:
A(int a)
:_a(a)
{
cout << "调用构造" << _a << endl;
}
~A()
{
cout << "析构成功" << endl;
}
private:
int _a;
};
int main()
{
A(2);
printf("此行用于检测匿名类是否生命周期只有一行\n");
return 0;
}
打印结果如图所示,匿名类在创建的时候我们没有给出对象名,而且生命周期确实只有一行。
6:拷贝对象时的VS编译器做出的优化
class A
{
public:
A(int a)
:_a(0)
{
cout << "调用构造" << _a << endl;
}
A& operator=(int a)
{
_a = a;
return *this;
}
~A()
{
cout << "析构成功" << endl;
}
private:
int _a;
};
int main()
{
A wjw(2);
wjw = 20;
return 0;
}
6.1:第一种优化场景
void fun1(A aa)
{}
int main()
{ A aa1(1);
fun1(aa1);
return 0;
}
这种场景先构造一个aa1对象,然后再用传值传参把aa1传给函数,C语言中我们学过栈帧,这里传值传参相当于拷贝了一份,但是我们所说的优化不是针对这种多行代码的,而是一行里面的连续表达式。否则优化了后面用到aa1对象该如何呢?
因此以下是可以优化的
fun1(A(1));
这里是 构造+拷贝构造 -> 构造。
这里其实也相当于fun1(1);因为之前类型转换的时候我们说过,实际上是先将1构造为一个无名类 然后再拷贝构造。
6.2:第二种优化场景
A fun2()
{
A aa;
return aa;
}
int main()
{
fun2();
return 0;
}
这里调用fun2函数是一个构造然后返回的时候也是一个临时拷贝 ,也就是构造+拷贝构造,但是这不是一个连续的表达式。
6.3:第三种优化场景
再看下面
A fun2()
{
A aa(1);
return aa;
}
int main()
{
A ret = fun2();
return 0;
}
这里是构造+拷贝构造+拷贝构造。这个时候拷贝构造会合并为1个拷贝构造,也就是构造+拷贝+拷贝->构造+拷贝。
再看下面
A fun3()
{
return A(1);
}
int main()
{
A ret = fun2();
return 0;
}
这里函数里面是一行了,不同于上面那段代码,这里是构造+拷贝+拷贝 ,触发编译器极致优化->构造。