在之前的两篇文章中介绍了有关C++中类和对象的大部分语法,本文对类和对象的语法内容做一些补充。这样就算初步掌握了面向对象的编程思想。
4.1再探构造函数
之前在实现构造函数时,进行变量初始化的方式是在函数体内进行赋值,但其实构造函数初始化还有一种方式,就是初始化列表。初始化列表有以下六个特点:
1.初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
2.每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
来手动实现一下初始化列表。下面的代码实现了时间类的初始化列表
#include <iostream>
using namespace std;
class Time
{
public:
Time(int hour, int minute)
:_hour(hour),
_minute(minute)
{}
void Print()
{
cout << _hour << "时" << _minute << "分" << endl;
}
private:
int _hour;
int _minute;
};
int main()
{
Time t1(19, 16);
t1.Print();
Time t2(19, 17);
t2.Print();
return 0;
}
运行结果
3.引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
这一点就是和默认构造的区别所在了。这三种类型的成员变量,是无法使用在函数体内赋值的方式初始化的,只能使用初始化列表进行初始化
class A
{
public:
A (int a, int b, int& c)
:_a(a),
_b(b),
_c(c)
{}
private:
int _a;
const int _b;
int& _c;
};
如果使用在函数体内赋值的方式进行初始化,则无法通过编译,并出现如下图所示的编译报错
4.C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
5.尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会进入初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造函数则会造成编译错误。
在下面的代码中,在初始化列表中对成员变量_day未做处理,但由于在声明时给了缺省值,就会用缺省值初始化
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year),
_month(month)
{}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 1900;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2024, 7, 18);
d1.Print();
return 0;
}
运行结果
如果在声明时不给缺省值,则会出现随机值的情况
6.初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
请看下面的代码,根据第6点可知,初始化列表的顺序与声明中成员变量出现的顺序一致,所以代码的执行顺序是,先将_a1的值赋给_a2,由于_a1未进行初始化,所以_a2的值为随机值,之后再将a的值,也就是1,赋值给_a1
class B
{
public:
B(int a)
:_a1(a),
_a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
B aa(1);
aa.Print();
return 0;
}
运行结果
但如果将成员变量的声明中,_a1和_a2的顺序调换一下,那么结果会完全不同,这里就不演示了,读者可以自行去尝试。
每个构造函数都有初始化列表,且每个成员变量都要使用初始化列表进行初始化,这里对初始化列表的调用顺序进行一下总结:
1.在初始化列表中的成员使用初始化列表进行初始化
2.没有在初始化列表的成员,在进行初始化时有以下两种情况:
(1)声明的地方有缺省值就使用缺省值进行初始化
(2)在没有缺省值时又有以下两种情况:
a.对于内置类型,初始化的情况取决于编译器,大概率是随机值
b.对于自定义类型,会调用它的默认构造函数,如果没有就编译报错
4.2静态成员
静态成员变量需要使用static关键字进行修饰,这样该变量会类似的成为全局变量,但它会受到类域的使用限制。这也是静态成员与全局变量的区别。使用static关键字修饰的成员变量有以下8个特点:
1.静态成员变量一定要在类外进行初始化。
2.静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
3.静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
来使用一下静态成员变量。在下面的代码中,使用static关键字修饰了一个成员变量,之后创建了三个C类的对象,相当于调用了三次默认构造函数,那么该静态成员也就被改变了三次
#include <iostream>
using namespace std;
class C
{
public:
C()
{
++_scount;
}
C(const C& t)
{
++_scount;
}
~C()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
// 类⾥⾯声明
static int _scount;
int _a;
};
// 类外⾯初始化
int C::_scount = 0;
int main()
{
cout << C::GetACount() << endl;
C a1, a2;
C a3(a1);
cout << C::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
运行结果
4.静态成员也是类的成员,受public、protected、private 访问限定符的限制。
5.突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
下面的代码中,想要直接访问静态成员,但静态成员虽然使用了static关键字进行修饰,但它受到了访问限定符的限制,无法直接访问,所以会出现编译报错。
//编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)
cout << C::_scount << endl;
6.使用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
7.静态成员函数中可以访问其他的静态成员,但是不能访问非静态成员,因为没有this指针。
8.非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
看下面代码的两种写法,第一种写法,由于static修饰了函数,所以在调用时并没有隐式的传入this指针,会出现报错。第二种写法都可以访问
static void Print() //编译报错,非法访问变量a
{
cout << _a << " " << _scount << endl;
}
void Print()
{
cout << _a << " " << _scount << endl;
}
4.3类型转换
在使用赋值运算符进行赋值时,当我们将一个float类型的值赋给int类型时,由于类型不同,在赋值的过程中会进行隐式类型转换。在C++中,使用类进行一些赋值操作时,也会产生类型转换。
在下面的代码中,明面上是直接将一个int类型的值赋给了一个类类型的对象,但其实,它在赋值的过程中会进行隐式的类型转换,从而达到我们想要的效果
#include <iostream>
using namespace std;
class A
{
public:
A (int a1)
:_a1(a1)
{}
A (int a1, int a2)
:_a1(a1),
_a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
A aa1 = 1;
aa1.Print();
return 0;
}
运行结果
上面展示的只是单参数的类型转换,在C++11之后,也支持多参数的隐式类型转换,这样写代码会更加的便利,减少代码冗余
int main()
{
A aa1 = { 1,1 };
aa1.Print();
return 0;
}
运行结果
如果不想让某个类支持类型转换,只需在构造函数前加上explicit关键字即可。
// explicit A(int a1)
//explicit A(int a1, int a2)
4.4友元
在之前实现日期类的运算符重载时,如果将重载函数放在类外面会无法访问类成员,进而造成一些限制。当时提供的方法是将运算符重载函数当做成员函数放入类中。这里还有一种方法,就是使用友元函数。友元提供了一种突破类访问限定符封装的方式。友元有以下8个特点:
1.友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
2.外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
下面的代码演示了友元的基本用法,在类体中声明友元函数,在函数体中访问类的成员变量
#include <iostream>
using namespace std;
class A
{
public:
friend void func(const A& aa); //友元函数声明
A (int a, int b)
:_a(a),
_b(b)
{}
private:
int _a = 1;
int _b = 1;
};
void func(const A& aa)
{
cout << aa._a << " " << aa._b << endl;
}
int main()
{
A a(2, 3);
func(a);
return 0;
}
运行结果
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4.一个函数可以是多个类的友元函数。
下面的代码中,在A类和B类中同时声明func函数为它们的友元函数,
#include <iostream>
using namespace std;
class B; // 前置声明,否则A的友元函数声明编译器不认识B
class A
{
public:
friend void func(const A& aa, const B& bb);
A (int a, int b)
:A_a(a),
A_b(b)
{}
private:
int A_a = 1;
int A_b = 1;
};
class B
{
public:
friend void func(const A& aa, const B& bb);
B (int a, int b)
:B_a(a),
B_b(b)
{}
private:
int B_a = 1;
int B_b = 1;
};
void func(const A& aa, const B& bb)
{
cout << aa.A_b << " " << aa.A_b << endl;
cout << bb.B_b << " " << bb.B_b << endl;
}
int main()
{
A a(2, 3);
B b(4, 5);
func(a, b);
return 0;
}
运行结果
5.友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
6.友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类并不是A类的友元。
下面的代码中,在A中声明B类是A类的友元,之后在B类中就可以直接访问A类的成员变量,但在A类中是不可直接访问B类的成员变量
#include <iostream>
using namespace std;
class A
{
public:
friend class B;
A (int a, int b)
:A_a(a),
A_b(b)
{}
void Print(const B& bb)
{
cout << A_a << " " << A_b << endl;
//cout << bb.B_a << " " << bb.B_b << endl; //不可访问B类的成员变量
}
private:
int A_a = 1;
int A_b = 1;
};
class B
{
public:
B (int a, int b)
:B_a(a),
B_b(b)
{}
void Print(const A& aa)
{
cout << B_a << " " << B_b << endl;
cout << aa.A_a << " " << aa.A_b << endl; //直接访问A类的成员变量
}
private:
int B_a = 1;
int B_b = 1;
};
int main()
{
A a(2, 3);
B b(4, 5);
b.Print(a);
return 0;
}
运行结果
7.友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是B的友元。
8.友元会增加耦合度,虽然提供了便利,但也破坏了封装,所以不宜多用。
4.5内部类
在C语言中,结构体内部是可以定义结构体的,允许进行嵌套。在C++中,类与类之间也是可以嵌套的。如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类有以下3个特点:
1.内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
2.内部类默认是外部类的友元类。
下面的代码中,在A类中定义了B类,并通过B类的成员函数func去访问A的成员变量,之后使用sizeof计算了A类的大小,观察结果发现,虽然B类是定义在A类的内部类,但它只受类域限制,该类本身是独立的
#include <iostream>
using namespace std;
class A
{
public:
class B
{
public:
void func(const A& aa)
{
cout << aa._k << " " << aa._a << endl;
}
private:
int _b = 0;
};
private:
static int _k;
int _a = 0;
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B bb; //通过类域访问符定义了B类
A aa;
bb.func(aa); //通过调用B类中的函数去访问A类的成员变量
return 0;
}
运行结果
3.内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其它地方都无法使用。
4.6匿名对象
C++允许我们定义匿名对象,就像C语言中可以定义匿名结构体。但匿名对象功能与Python中的lambda表达式有点类似,适应于临时定义一个对象进行简单使用的场景。匿名对象有以下2个特点:
1.使用以下的代码格式定义出来的对象叫做匿名对象,而使用之前的格式定义出来的对象叫有名对象。
类名([参数]);
A(); //定义不带参数的匿名对象
B(1); //定义带参数的匿名对象
2.匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
在上文介绍内部类时,主函数中有一段比较冗余的代码,为了调用B类中的func()函数,定义了两个有名对象,最终才达到了目的,如果使用匿名对象,就可以让代码变得简化一些
//这是上文中使用有名对象调用func()函数的代码
int main()
{
A::B bb;
A aa;
bb.func(aa);
return 0;
}
//使用匿名对象调用func()函数的代码
int main()
{
A::B().func(A());
return 0;
}
可以看到代码变得非常的简洁。
总结一下,本文对于C++中有关类和对象的细枝末节的一些语法点进行了介绍与总结,初始化列表在以后的编程中会经常用到,应当熟练掌握。而其它的语法点,在一些特定场景下会有妙用,这就需要读者去自行在实践中积累经验了。