前言:本文将补充C++类与对象部分剩下的一些零散知识点,类与对象就算是告一段落了
1.构造函数的补充知识
在之前的类中,我们主要是通过在函数类赋值来完成初始化。除了这种初始化的方法外,C++其实还提供了另外一种初始化的方式,这种方式被称为初始化列表。用法就是以一个冒号开始然后以逗号作为分隔,每个显式写的成员变量跟一个括号(),里面可以放变量 或者表达式
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year) //初始化列表
, _month(month) //可以放变量或者表达式
, _day(day)
{
}
void Pirnt() const
{
std::cout << _year << "/" << _month << "/" << _day;
std::cout << std::endl;
}
private:
int _year;
int _month;
int _day;
};
初始化列表并没有表面上看起来这么简单,它还有以下几个小点需要注意:
- 每个成员变量只能在初始化一次,在语义上我们认为初始化列表是成员变量定义的地方(尽管成员变量的空间在函数栈创建时就开好了),所以既然是定义的地方每个成员自然只能出现一次
- 如果类里面有const成员变量时,我们一定要保证这个const变量一定要在初始化列表被初始化,因为const成员不像其他的普通变量一样不初始化也可以不给就会编译报错:
cpp
// error:C2789未提供初始值设定项
- 引用类型也是同理在定义时必须要初始化,否则都不知道要引用谁
除此之外初始化列表不是只能想我这样竖着写,横着写也是可以到但是变量多了就不好看这个主要看你喜好,横着写:
cpp
Date(int year = 1, int month = 1, int day = 1)
:_year(year) , _month(month), _day(day)//和竖着写一样
{
}
C++11提供了一种在成员变量声明处给缺省值的做法,但是需要注意的是这个缺省值是专门给没有显式在初始化列表写的变量 使用的这里可千万不要 把这个和构造函数参数列表给的缺省值给搞混了,比如下面的代码
cpp
{
public:
//我这里没有显式的写初始化列表,在实例化对象
//时也没有传参所以没有使用参数列表的缺省值
Date(int year = 1, int month = 1, int day = 1)
{
//在函数体内部什么都没有
}
void Pirnt() const
{
std::cout << _year << "/" << _month << "/" << _day;
std::cout << std::endl;
}
private:
// 成员变量声明
// C++11提供给初始化列表使用的缺省值
int _year = 2;
int _month = 2;
int _day = 2;
};
int main()
{
Date d;
d.Pirnt();
return 0;
}
那么运行程序是 1 1 1还是2 2 2呢?我们运行一下程序:

这是因为虽然对象是实例化时会调用构造函数并且这个构造函数也给了缺省值但是因为我们在函数里面什么都没干所以相当于这个构造函数的参数列表什么用都没有。
那么为什么我们明明没有在初始化列表显式的初始化这些成员变量,这些成员变量还是被初始为了2呢?这是因为我们不管是显式的写了还是没写每个成员变量都会走初始化列表 ,并且初始化的顺序是按照成员变量声明的顺序来进行初始化的。
对于内置类型 来所,如果你不显式的写同时也没有在声明的地方给缺省值的话,怎么处理在C++标准中并没有具体的规定,不同的编译器对内置类型的处理都不同。而对于自定义类型来说,如果没有显式的写而且还没有默认的构造函数时就会报错,以下面的代码来举一个例子:
cpp
class Time
{
public:
Time(int hour) // 这个并不是默认成员函数,要传参数
:_hour(hour)
{
std::cout << "Time()" << std::endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
,_t(1) // 因为Time类没有默认构造函数,所以这里要手动调用
{
// ...
}
void Print() const
{
std::cout << _year << "/" << _month << "/" << _day << std::endl;
}
private:
// 声明
int _year;
int _month;
int _day;
Time _t;
};
在上面的代码中Date类中又自定义成员变量Time,但是Time类没有默认构造函数,如果没有,_t(1)显式调用的话就会编译报错
总结:

2.类的隐私类型转化
C++是支持把内置类型隐式转化为类类型的对象,但是对应的类中必须要有相关内置类型为参数的构造函数,否则就会编译报错。下面统一以这段代码来为大家说明:
cpp
class A
{
public:
A(int a1)
:_a1(a1)
{
// ...
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
// ...
}
void Print()
{
std::cout << _a1 << " " << _a2 << std::endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{
// ...
}
private:
int _b = 0;
};
因为A类中关于内置类型int的构造函数,我们可以这么快速的创建对象:
cpp
// 相当于是把3变成了一个A类的临时对象
// 然后把临时对象拷贝的a里
A a = 3;
a.Print();
编译器遇到连续构造加拷贝构造情况往往会直接优化为直接构造,但也不要忽略临时对象的存在,比如遇到下面的情况:
cpp
//这样编译会报错,因为临时变量具有常属性
A& a1 = 2;
//需要使用const引用
const A& a2 = 2;
C++11引入了可以支持多参数转化的能力,同样的我们也要有支持多参数的构造函数,比如上面A类中的
cpp
A(int a1, int a2)//支持多参数转化
:_a1(a1)
, _a2(a2)
{
// ...
}
用这种隐式转化构造对象时要使用花括号:
cpp
A a1 = { 2, 4 };//C++11引入的方式
a1.Print();
类类型之间也支持相互转化,同样需要有相应的构造函数。比如在上面的B类中就有类A的构造函数,所以我们可以把A类转化为B类:
cpp
B(const A& a)
:_b(a.Get())
{
// ...
}
使用的方式和上面的转化很像:
cpp
//类之间的隐式转化
A a1 = { 2, 4 };
B b1 = a1;
3.static成员
被static修饰的成员变量被称之为静态成员变量,静态成员变量一定要在类外面进行初始化 ,因为静态成员变量是为所有类所共享的,不属于某个类的具体对象,不存在对象中,被存放在静态区中。我们可以写一个程序来验证一下:
cpp
class A
{
int a;
static int b;//静态成员变量
};
int main()
{
A a1;
std::cout << sizeof(a1) << std::endl;
return 0;
}
//在类外面初始化
int A::b = 0;
根据内存对齐的规则,如果b不是静态成员变量的话那么打印的结果应该是8个字节才对,但是因为成员变量b被static修饰了变成了静态成员变量被放在静态区而不是在A类的对象中,所以打印出的字节大小因该是4才对。我们运行程序验证下我们的猜想:

结果完全符合我们的预期
static除了可以修饰成员变量外,还可以修饰函数。被修饰的函数被称之为静态成员函数,静态成员函数没有this指针。静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。但是非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
想要访问静态成员函数或者变量也很简单,只需要突破类域的限定就可以我们可以通过::静态成员或者对象.静态成员即可访问静态成员或者函数。因此我们就可以认为静态成员变量或者函数只是受到类域的限制 ,同样会受到public、protected、private 访问限定符的限制。
我们可以通过这道经典oj题来将我们学到的知识运用起来求1+2+3+...+n:
cpp
class sum
{
public:
sum()
{
_ret += _i;
++_i;
}
static int Getret()
{
return _ret;
}
private:
static int _ret;
static int _i;
};
int sum::_ret = 0;
int sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
sum a[n];
return sum::Getret();
}
};
解决这道题的思路就是利用每次创建对象都要调用构造函数,设置两个全局静态变量来统计结果。
4.友元与内部类
4.1友元函数与友元类
友元可以突破类访问限定符的封装,友元分为友元函数和友元类,在函数或者类声明前面加上friend,并放着某个类中,被friend修饰的函数或者类就是这个类的友元函数或者友元类。这样这个被修饰的函数或者类就可以任意访问类中私有或者保护的成员,但是需要注意的是友元函数仅仅是一种声明它可不是这个类的成员
友元函数使用:
cpp
//B类的前置声明,要不然编译器不认识
class B;
class A
{
public:
//友元声明
friend void func(const A& a, const B& b);
private:
int _a = 1;
};
class B
{
public:
//友元声明
friend void func(const A& a, const B& b);
private:
int _b = 2;
};
// func函数是A类与B类的友元函数
void func(const A& a, const B& b)
{
std::cout << a._a << "/" << b._b << std::endl;
}
int main()
{
A a;
B b;
func(a, b);
return 0;
}
运行程序输出1 2证明了这个函数可以访问A对象与B对象的私有变量,对于类也是一样的我这里就不多做过多的演示了,关于友元之间的关系还有以下的几个小点:
- 一个函数可以是多个类的友元函数,比如上面的func就是A类与B类的友元函数
- 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员
- 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元
- 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元
友元虽然可以让我们可以更加方便的访问类中的成员或者方法,但是同时会破坏这个类的封装,可能会导致存在风险问题,所以使用的时候要慎重
4.2内部类
如果一个类定义在另外的一个类的内部,那么那个在内部的类就叫做内部类,内部类是一个单独的类,和定义在全局相比它只是受到外部类的类域与访问限定符的限制,所以外部类定义出的对象并不包含内部类,而且内部类默认就是外部类的友元类。
我们可以写代码来验证一下:
cpp
class A
{
int a;
class B
{
int b;
};
};
int main()
{
A tmp;
std::cout << sizeof(tmp) << std::endl;
return 0;
}
运行程序输出4,表明了它们不是同一个类
在之前求阶乘求和的那道题中,我是定义出的sum类就是为了完成Solution类里的Sum_Solution方法求和的一个辅助类,所以我们可以之间把sum类定义为Solution的内部类:
cpp
class Solution {
public:
class sum
{
public:
sum()
{
_ret += _i;
++_i;
}
};
static int _ret;
static int _i;
int Sum_Solution(int n) {
sum a[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
5.匿名对象
匿名对象比较的简单,我们用类型(实参) 定义出来的对象叫做匿名对象 ,相比之前我们定义的类型 对象名(实参) 定义出来的叫有名对象。匿名对象的生命周期只有一行,下一行就会自动的析构掉。
代码演示:
cpp
class A
{
public:
A(int a = 0)
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
};
int main()
{
//匿名对象,生命周期只有一行
A(1);
//走到这里这个匿名对象就被析构了
std::cout << "你好啊" << std::endl;
return 0;
}
运行程序验证一下:

我们平常长期用的水壶就像是有名对象一样可以相对长期的使用,而匿名对象就像是一次性水杯一样,喝完这杯水就丢了
完