嗨喽,今天阿鑫给大家带来的是类和对象第五弹,下面让我们进入今天内容的学习吧!
类和对象第五弹
1. 再谈构造函数
2. static成员
3. 友元
4. 内部类
1.1初始化列表
class Stack
{
public:
Stack(size_t capacity )
{
cout << "Stack(size_t capacity = 3)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == NULL)
{
perror("malloc failed");
return;
}
_capacity = capacity;
_size = 0;
}
class MyQueue
{
public:
private:
Stack _pushet;
Stack _popst;
int _size;
};
我们首先可以看到这样一个类,在之前的博客我们提到了在创建一个MyQueue类时,可以不用写构造函数,利用编译器自动生成的默认构造函数,但是如果我们给出的自定义类型成员栈的构造函数如果不是默认构造怎么办?此时就需要我们自己写队列的构造函数,对于带参的自定义类型成员,我们提出了一个新的构造函数的方法,就是初始化列表。
我们先来理清楚构造函数的初始化列表和函数体内赋值的区别
很多同学都搞不懂为什么初始化列表是定义,而函数体内是赋值,下面我通过几个例子来给大家说清楚
赋值与初始化:
初始化是创建变量时赋予其一个初始值。在类的构造函数中,成员变量通常通过初始化列表进行初始化。
赋值是在变量已经存在后改变其值。
MyQueue(int n)
:_pushet(n)
,_popst(n)
{
_size = 0;
}
1.pushet(n) 和 _popst(n) 是成员变量的初始化,它们在构造函数体执行之前完成。
2._size = 0; 是在构造函数体内部执行的赋值操作。此时,_size 成员变量已经由编译器在对象创建时自动初始化(如果是基本类型,通常是默认初始化)。然后,通过这行代码,我们将 _size 的值设置为 0。
编译器确实对内置类型(如 int、char、float 等)的成员变量有默认初始化行为。对于类中的内置类型成员变量,如果在构造函数初始化列表中未显式初始化,那么它们将进行默认初始化。
对于局部内置类型变量(即非静态局部变量),如果没有显式初始化(即初始化列表初始化),它们将包含未定义的值(即所谓的“垃圾值”)。
对于类的成员变量,当创建类的对象时,这些成员变量会进行默认初始化。对于内置类型,默认初始化实际上意味着它们将不会被赋予任何特定值,它们将包含垃圾值。然而,如果你在构造函数体内部给它们赋值,那么这些垃圾值将被覆盖。
重要的是要区分构造函数初始化列表和构造函数体。初始化列表用于在构造函数体执行之前初始化成员变量,而构造函数体中的代码则在所有成员变量初始化之后执行。
下面我们通过两个例子来更好地说明
class MyClass {
public:
MyClass() {
// 构造函数体开始执行
// 此时,成员变量已经被初始化(对于内置类型,是默认初始化)
x = 0; // 赋值操作
}
private:
int x; // 成员变量声明
};
在上面的例子中,当创建 MyClass 的对象时,成员变量 x 会被默认初始化(对于 int 类型,这意味着它包含垃圾值)。然后,在构造函数体中,我们将 x 的值设置为 0(此为赋值)。
然而,更好的做法是使用初始化列表来初始化成员变量,特别是当你有控制权时(即当你编写类的定义时):
class MyClass {
public:
MyClass() : x(0) {
// 构造函数体开始执行
// 此时,x 已经被初始化为 0
}
private:
int x; // 成员变量声明
};
也就是说,构造函数体中的代码则在所有成员变量初始化之后执行,也就是初始化列表你就算没写他也会相当于把所有的成员变量都展开,相当于把所有的成员变量都初始化,如果内置类型你没有初始化或没有给缺省值,就会默认为垃圾值但也相当于定义了出来,所以函数体内只能算是赋值,可以把之前的垃圾值给覆盖住
缺省值是给初始化列表用来初始化成员变量的。所以如果一个类没有默认构造,不传缺省值并且也没有在初始化列表初始化的话会报错,但传了缺省值就会调用它的非默认构造函数。
三个必须写在定义处的情况
1.const intx
2int& ref;
3没有默认构造的自定义类型的成员变量(必须显示传参构造)
因为const变量和引用必须在定义时就初始化,如果写在函数体内就相当于是赋值的时候初始化,程序会报错。
引用同理
下面给出如果我们没有写初始化列表的情形
1.初始化列表,不管你写不写,每个成员变量都会先走一遍(按照声明的顺序)。
2.自定义类型的成员会调用默认构造(如果没有默认构造就报错)。
3.内置类型有缺省值用缺省值,没有的话,不确定看编译器,有的编译器会处理,有的不会处理。
4. 先走初始化列表 +再走函数体。
所以我们最好利用初始化列表对成员变量进行初始化。
其实初始化列表的构造函数不是一种新的构造,相反他是一直存在的,只不过之前我们并没有介绍初始化列表,同学们不要将他与前面的搞混,我们可以给出一个具体的例子
Date(int year,int month,int day)
//初始化列表,这里会依次对成员变量进行初始化,
// 所以函数体内的都算是赋值,
//原先就是有初始化列表,只是我们没有进行介绍
{
_year = year;
_month = month;
_day = day;
_day = year;
}
1.2构造函数之声明与定义顺序
class A2
{
public:
A2(int a)
:_a1(a)
,_a2(_a1)
{
}
void Print()
{
cout << _a1<<" "<<_a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A2 aa1(1);
aa1.Print();
return 0;
}
同学们可以猜猜我们运行的结果是多少?
答案是1和随机数
在这里我们要提出一个概念;成员变量在类中的声明顺序就是初始化列表中初始化的顺序,与其在初始化列表中的顺序无关,所以我们在日常性写代码就要保证自己声明成员变量的顺序,防止发生意想不到的错误。
1.3利用初始化列表进行隐式类型转换以及explicit关键字
下面我们通过几个例子来说明什么是隐式类型转换,以及单参数类型转换和多参数类型转换
class A
{
public:
//explicit
//单参数类型转换
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)//拷贝构造
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
private:
int _a;
int _a1;
int _a2;
};
A aa1(1);
A aa2 = aa1;//拷贝构造
A aa3 = 3;//先构造函数创建一个A类型的临时变量,用来接收3,然后再传值调用进行拷贝构造
const A& aa4 = 3; //只有构造函数,但是需要加const
单参数类型转换
从上到下依次是
1.构造函数
2.拷贝构造
3.先构造函数创建一个A类型的临时变量,用来接收3,然后再传值调用进行拷贝构造
4.利用整形3构造一个A类型,将aa4作为该临时变量的引用,由于临时变量的常性,所以我们需要加上const。
多参数类型转换
//多参数类型转换
A(int m, int n)
:_a1(m)
,_a2(n)
{
_a = 0;
}
A aaa1(1, 2);
A aaa2 = { 1,2 };
const A& aaa3 = { 3,4 };
我们只需要继续加上这几行代码,就可以实现多参数的类型转换,这对我们之后的学习用处非常大
当然,如果你不想进行隐式类型转换,你可以在构造函数名前加上explicit
explicit Date(int year)
:_year = (year)
{}
1.1static成员概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
1.2static成员特性
1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3.类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5.静态成员也是类的成员,受public、protected、private 访问限定符的限制
下面我们通过一个题目来引出static的用法
面试题:实现一个类,计算程序中创建出了多少个类对象。
class AA
{
public:
AA()
{
++_scount;
}
AA(const AA& t)
{
++_scount;
}
~AA()
{
--_scount;
}
static int GetCount()
{
return _scount;
}
private:
int _a1 = 1;
int _a2 = 1;
static int _scount;
};
int AA:: _scount = 0;
该代码可以算出实时存在的对象个数。
并且这里我们需要注意static函数没有this指针,所以调用的时候与正常的成员函数不同
int GetCount()
{
return _scount;
}
static int GetCount()
{
return _scount;
}
AA aa1;
cout << aa1._GetCount() << endl;
cout << AA::GetCount() << endl;//需要指定类域
1.3c++成员函数的调用
在C++中,并不一定需要对象来调用成员函数。这完全取决于成员函数的类型:静态成员函数或非静态成员函数。
1.非静态成员函数:这些函数需要一个类的实例(对象)来调用。这是因为非静态成员函数通常与特定对象的状态相关,它们可以访问和修改对象的非静态成员变量。例如:
class MyClass {
public:
void myFunction() {
// ...
}
};
int main() {
MyClass obj; // 创建一个对象
obj.myFunction(); // 使用对象调用非静态成员函数
return 0;
}
2.静态成员函数:静态成员函数属于类本身,而不是类的任何特定对象。因此,它们可以通过类名直接调用,而不需要创建类的对象。静态成员函数只能访问静态成员变量和其他静态成员函数,不能访问非静态成员变量或成员函数。例如:
class MyClass {
public:
static void myStaticFunction() {
// ...
}
};
int main() {
MyClass::myStaticFunction(); // 直接通过类名调用静态成员函数
return 0;
}
总结来说,静态成员函数不需要对象来调用,而非静态成员函数则需要。
3.1友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
我们在之前实现日期类将日期打印出来的时候,其实已经利用过友元函数了,本质是为了避免隐藏的this指针影响我们代码的可读性,下面我们给出代码
friend ostream& operator<<(ostream& out, const Date& d);//此处第一个参数必须传引用
friend istream& operator>>(istream& in, Date& d);//此处第一个参数必须传引用
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
cout << "请输入年月日:" << endl;
in>> d._year >> d._month >>d._day ;
return in;
}
3.2友元类
1友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
class Time
{// 声明 Date是Time的
friend class Date;
public:
Time(int hour = 0, int minute = 0,int second = 0)
:hour(hour)
, minute(minute)
, second(second)
{}
private:
int hour;
int minute;
int second;
};
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递,如果C是B的友元,B是A的友元,则不能说明C时A的友元
3.友元关系不能继承,在继承位置再给大家详细介绍。
但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
4.1内部类概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
4.1内部类特性:
1.内部类可以定义在外部类的public、protected、private都是可以的。
2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3.sizeof(外部类)=外部类,和内部类没有任何关系。
外部类只会改变内部类的类域,调用B当中的成员函数需要现在A的类域中寻找
class A3
{
public:
class A3(int n)
:h(n)
{
}
class B//B天生就是A的友元
{
public:
B()
{
}
void foo(const A3& a)
{
cout << k << endl;//oK
cout << a.h << endl;//oK
}
//B能访问A的私有,A不能访问B的私有
};
private:
static int k;
int h;
};
int A3::k = 1;
int main()
{
A3 a1(5);
A3::B b1;//
b1.foo(a1);