IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 语法之类和对象基础篇详解 -> 正文阅读

[C++知识库]C++ 语法之类和对象基础篇详解

??个人主页:欢迎大家光临——>沙漠下的胡杨

? 各位大帅哥,大漂亮

?如果觉得文章对自己有帮助

?可以一键三连支持博主

?你的每一分关心都是我坚持的动力

?

??: 本期重点:C++类和对象

??希望大家每天都心情愉悦的学习工作。


?

目录

??: 本期重点:C++类和对象

面向对象和面向过程:

类的引入:

类的定义:

类的访问限定符:

封装的概念:

类的实例化:

类对象模型:

this 指针:

类的默认成员函数

我们类的默认函数主要以date和stack为研究

类的构造函数:

析构函数:

? 拷贝构造函数:

运算符重载:

前置++ 和 后置++

const 成员函数:

取地址运算符的重载:

初始化列表:

explicit关键词:

static成员:

友元:

我们简单实现下日期类:

编译器的优化:


面向对象和面向过程:

我们C语言是面向过程的语言,关注的是过程。分析出求解问题的步骤,通过函数调用来逐步解决问题。

C++则是基于面向对象,关注的是对象。将一件事情拆分成不同的对象,靠对象之间的交互完成。

对比这来说:我们点外卖时,我们C语言关注的点餐,接单,送餐系统,关注这些流程的实现。我们C++则是关注用户(点餐),商家(接单)和骑手(送餐系统),关注这三个对象之间的关系。

??

类的引入:

C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。 比如:用C 语言方式实现的栈,结构体中只能定义变量 ;现在以 C++ 方式实现,会发现 struct 中也可以 定义函数
我们C语言定义Stack 如下所示,使用一个结构体,内部放变量,外部写函数。
  struct Stack    
  {    
      int val;    
      int top;    
      int capaticy;    
  }    
      
  void push()    
  {}    
  void pop()    
  {}    
  void top()    
  {}   

我们C++则是可以把外部的函数,也放在struct中。

  struct Stack    
  {    
      int val;    
      int top;    
      int capaticy;
      void push()
      {}
      void pop()
      {}    
      void top()
      {}
 }

而我们C++中更喜欢使用class来定义类,一般不怎么使用struct。

类的定义:

class classname
{
    //类体:成员变量,成员函数
};//注意分号
class 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分号不能省 。其中我们类的主体叫做类域,我们在外部访问类域时,需要使用预作用限定符,来指明是那个类域的。
类体中内容称为类的成员: 类中的 变量 称为 类的属性 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数
?
类的定义的两种方式:
1.声明和定义全部放在类体里面,非常小的类建议这样定义。其中,类中的成员函数将会默认加了linline,如果符合内联函数的所有要求,那么就会变成内联函数。
2.我们把声明和定义分离,一般我们使用这种方法来定义类。这样显得更加简洁,也方面我们阅读代码。我们分离时要注意,我们的成员函数前,要加上类名,否则就不能知道是哪一个类的函数,就会报错。如果其中一个成员函数想变成内联函数,那么这个成员函数就不用声明定义分离啦。

类的访问限定符:

1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 ( 此处 protected private 是类似的 )
我们在类里面是没有所谓的访问限定的,在类里面所有的成员变量和成员函数都可以任意访问。类就像是自己家的门一样,门外面的人访问需要邀请,而门里面的人则可以随意访问。
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束为止。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

封装的概念:

我们在类和对象篇重点认识的是封装,封装是什么呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
C++ 语言中实现封装,可以 通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内 部实现细节,控制哪些方法可以在类外部直接被使用
?
封装的本质是管理,是我们更为方便的使用类,就比如我们取栈顶的元素来说,我们一般来说要提供一个函数的接口来进行读取,如果我们不进行封装,那么我们要取数据就只能自己去栈顶取数据,这样就会很可能导致出错误,如果我们提供了这个函数的接口,那么我们的函数逻辑只要正确,那么就很难出现错误。这也是一种封装,我们提供啦这个函数接口,也就减少啦访问数据错误的次数。

类的实例化:

? ? ? ?我们使用类来进行创建一个类的对象的过程,叫做类的实例化,在类中定义的变量,都是属于变量的声明,我们一个类是没有占用内存的,只有类实例化出来的对象才占有空间,但是我们可以通过类中变量的声明来进行获取要开辟的空间的大小,就比如一个 int 我们就会开4字节空间,一个 double 就会开8字节空间的大小,同样的一个类我们知道它的声明,就可以知道他要开多大的空间。

类对象模型:

首先我们的类的大小是怎么计算的呢?

我们先看个例子,我们理解下类的存储模型。

class A
{
public:
	void func()
	{
		cout << "func():" << endl;
	}
private:
	char a;
};

int main()
{
	A aa1;
	cout << sizeof(aa1) << endl;
	return 0;
}

输出的结果是 1 ,这个结果也说明了,在计算类的大小时我们没有把成员函数加上去,我们只计算了成员变量的大小,那我们是怎么找到成员函数的呢?或者说类是怎么存储的呢?

可能有下列3种方法存储:

1. 我们直接把成员函数和成员变量存在一起(不采取,上面例子说明啦)

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

? ? ? ? ? ?

2.代码只保存一份,对象中保存存放代码的地址,这个就比较合适。

3. 成员函数和成员变量分开保存。

? ? ? ? ? ? ? ? ? ???

下面我们来探究下计算机是采用方法 2 ,还是方法 3 存储的。

我们方法 2 中说,我们的类中存储的是成员变量和存放成员函数的地址,那么也就是说我们访问成员函数时需要进行解引用操作,我们都知道空指针不能解引用,我们看下面例子:

class A
{
public:
	void func()
	{
		cout << "func():" << endl;
	}
private:
	char a;
};

int main()
{
	A* ptr = nullptr;
	ptr->func();
	return 0;
}

?这个代码是可以正常运行的,由此我们得出结论,我们的计算机存储类是按照第三种方式进行存储的,因为上面的代码如果是第一种或者是第二种程序都会崩溃。

我们的类也应该遵守 struct 时内存对齐的规则,也就是我们计算类的大小时,我们只计算成员变量,并且进行内存对齐。我们再来复习下内存对齐的规则:

1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
5. 另外我们还可以使用 #pragma pack (要修改默认对齐数大小),来进行修改默认对齐数。

如下所示:?

此时默认对齐数是8(VS默认的)

class A
{
private:
	char a;
};

class B
{
private:
	char a;
	int b;
};

class C
{
private:
	char a;
	int b;
	double c;
};

int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
	return 0;
}

?

如果我们修改默认对齐数,改为1,此时就会紧挨着存放。

#pragma pack(1)
class A
{
private:
	char a;
};

class B
{
private:
	char a;
	int b;
};

class C
{
private:
	char a;
	int b;
	double c;
};

#pragma pack()

??

还有一个比较特殊的类,就是空类:

在C++下,空类和空结构体的大小是1(编译器相关),这是为什么呢?为什么不是0?这是因为,C++标准中规定,“no object shall have the same address in memory as any other variable” ,就是任何不同的对象不能拥有相同的内存地址。 如果空类大小为0,若我们声明一个这个类的对象数组,那么数组中的每个对象都拥有了相同的地址,这显然是违背标准的。所以我们需要给空结构体,或者空类加上那个一个虚设的字节。

this 指针:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "." << _month << "." << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Init(2022, 9, 12);
	
	Date d2;
	d2.Init(2022, 9, 9);

	d2.Print();
	d1.Print();

	return 0;
}

上面是一个简单的日期类的定义,我们在调用d1的 Init 时,这个函数怎么知道设置的对象是d1,而不是设置d2呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该 指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

就是我们在调用 d1.Init 时,我们传递的参数是 d1.Init (&d1 , 2022 , 9 , 12),我们默认会传递其中d1的地址,然后调用时是使用 this 指针进行指向具体的成员变量。我们实参和形参位置? 不能显示的传递和接收 this 指针,但是可以在成员函数内部使用 this 指针(可以任意的使用或者不使用 this 指针指向成员变量)? ? ?

void Init(int year, int month, int day)
{
	_year = year;
	this->_month = month;
	this->_day = day
}

this指针的特性:

1 . this 指针的类型是 (类名? * const? this),就是表明我们在成员函数中不能对 this 进行赋值,它是只读属性的。

2. this 只能在成员函数的内部进行使用。

3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。 所以对象中不存储this指针。

4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。?

this指针存在哪里呢?

我们一般情况下,this 是存在栈区的,因为我们的 this 终归是一个局部变量,但是我们的编译器会做出优化,可能会让 this 指针放在寄存器中,来加快访问速度。

this 指针可以为空吗?

我们在谈论类的存储时,我们看到了下面的代码:

class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

上面代码中,我们能够得出结论 this 指针是可以为空的,因为我们在程序编译链接时期,我们的Print()函数的地址就已经固定啦,我们是直接 call 这个函数的地址来进行调用的,此时我们也没有进行解引用这个空指针,并且后续我们访问成员函数时,我们的成员函数中没有成员变量,就不涉及到解引用this指针的问题,虽然我们的 this 指针为空,但是没有解引用,就不会导致程序崩溃。

我们再看下列代码:

class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

这个代码是会运行崩溃,因为我们的this指针是可以为空指针的,但是我们不能进行解引用访问,如果我们的this指针为空,并且我们需要访问有成员变量的成员函数时,就会产生解引用空指针,进而导致程序崩溃。

类的默认成员函数

一个类中什么成员都没有,我们称之为空类,我们已经知道了,空类的大小为1,但是空类中真的什么都没有吗?

并不是,任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

我们的重点是构造函数,析构函数,拷贝构造和运算符重载。?

我们类的默认函数主要以date和stack为研究

??

class Date
{
public:
 void Init(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 void Print()
 {
 cout << _year << "-" << _month << "-" << _day << endl;
 }
private:
 int _year;
 int _month;
 int _day;
};
typedef int DataType;
class Stack
{
public:
 Stack(size_t capacity = 3)
 {
    _array = (DataType*)malloc(sizeof(DataType) * capacity);
    if (NULL == _array)
    {
        perror("malloc申请空间失败!!!");
        return;
    }
    _capacity = capacity;
    _size = 0;
 }
    void Push(DataType data)
    {
    // CheckCapacity();
     _array[_size] = data;
     _size++;
    }
 // 其他方法...
private:
 DataType* _array;
 int _capacity;
 int _size;
};

类的构造函数:

构造函数的概念:

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免 有点麻烦,那能否在对象创建时,就将信息设置进去呢? 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成 员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

构造函数的特性:

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开 空间创建对象,而是初始化对象。 其特征如下:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定 义编译器将不再生成。

6.C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如: int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,就会发现编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数,对内置类型不做处理。

??

7.注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

?8.虽然我们的默认构造函数可以进行函数重载,但是我们的全缺省的默认构造,和无参的默认构造会起冲突,就是调用过程中就会起冲突。最好的是提供一个全缺省的默认构造。

默认构造函数的特点:

析构函数:

析构函数概念:

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的? 析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成 的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数的特性 :

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

?

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5. 关于编译器自动生成的析构函数,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

? 拷贝构造函数:

拷贝构造函数概念:

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型 对象创建新对象时由编译器自动调用。

拷贝构造函数特征如下:

1. 拷贝构造函数是构造函数的一个重载形式。

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发 无穷递归调用。

我们如果使用传值返回会导致无穷递归,因为我们要拷贝就要传值,但是传值的过程就又是一次拷贝构造,这样就形成无穷递归啦。如果我们传引用就可以解决这个事情。我们的传地址也是可以解决的,但是那样的话就不符合拷贝构造的特性啦。

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,当然像日期类这样的类是没必要的。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,我们自己写的要是的是深拷贝,否则就是浅拷贝。?

首先,我们的栈实现的是一个浅拷贝,也就是会直接拷贝地址来赋值给我们的另一个对象,这样形成的是一个浅拷贝,然后我们如果是没有资源清理的,那就没事,如果我们需要进行资源清理,那么我们必须要进行深拷贝,否则就会出现上述情况,我们析构时我们就会析构两次,导致访问空指针吗,程序崩溃。

5. 拷贝构造函数典型调用场景:

使用已存在对象创建新对象

函数参数类型为类类型对象

函数返回值类型为类类型对象

注意:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。?加上const 之后就会防止我们出现以下的情况:

运算符重载:

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。 函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型?operator操作符(参数列表)

注意:

不能通过连接其他符号来创建新的操作符:比如operator@

重载操作符必须有一个类类型参数

用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

?.*? ? ? ::? ? sizeof? ? ? :? ? .? ? ?注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

我们使用运算符重载时,我们一般要重载为成员函数,因为我们重载成全局的,就要成员变量是公有的,这样我们破坏了封装,所以我们一般重载为成员函数,或者使用友元解决。

1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义

	Date operator=(const Date& d1)
	{
		if (this !=  &d1)
		{
			this->_year = d1._year;
			this->_month = d1._month;
			this->_day = d1._day;
		}
		return *this;
	}

我们的赋值支持连续赋值,所以我们的返回值要是Date 类型的,并且我们赋值时,虽然有两个操作数,但是我们有一个默认的this指针,所以我们只需要接受一个值就可以啦。最好我们

传引用值就好啦,而且我们一般传引用要传const引用。

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
   if (&left != &right)
   {
       left._year = right._year;
       left._month = right._month;
       left._day = right._day;
   }
   return left;
}
// error C2801: “operator =”必须是非静态成员

首先,我们赋值运算符在类外部是有两个参数的,所以我们要传两个参数,但是还是会编译报错。原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

这个其实和拷贝构造类似,我们赋值运算符重载时也要考虑深浅拷贝的问题,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。


?总的来说,我们赋值运算符对于有资源需要清理的类,需要自己写赋值运算符重载,然后我们还要进行深拷贝赋值,不能进行字节直接拷贝。

前置++ 和 后置++

我们是怎么进行区分前置++和后置++呢?

前置++:返回+1之后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率。


后置++:返回+1之前的结果
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数传递,因为我们编译器不支持(a++)++这种语法,所以我们一般返回类型设置为const 。

	Date operator++()//前置
	{
		return *this += 1;
	}

	const Date operator++(int)//后置
	{
		Date ret = *this;
		*this += 1;
		return ret;
	}

注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需,然后this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。并且我们一般使用时还是要使用前置++,因为前置++没有拷贝构造和析构,效率高。?

const 成员函数:

我们知道,在C++中,若一个变量声明为const类型,则试图修改该变量的值的操作都被视编译错误,虽然我们也可以通过其他的方法进行修改,但是我们加上const之后,就不希望我们这个变量被修改。

在C++中,只有被声明为const的成员函数才能被一个const类对象调用。要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const。我们将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

我们的const 成员函数和非const成员函数可以进行重载:

1. const成员函数可以访问非const对象的非const数据成员、const数据成员。也可以访问const对象内的所有数据成员;

2. 非const成员函数可以访问非const对象的非const数据成员、const数据成员。但不可以访问const对象的任意数据成员;

3. 作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const?成员函数。

注意:把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象。

取地址运算符的重载:

一般情况下,我们的取地址运算符和const取地址运算符不需要自己写,默认提供提供的就足够啦,除非我们不想让别人获取我们的指定的地址或者内容。

Date* operator&()
{
    return this ;
}
const Date* operator&()const
{
    return this ;
}

初始化列表:

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值,对于以下的情况下,我们需要初始化列表来进行初始化工作。
1. 引用成员变量
2.?const成员变量
3. 自定义类型成员(且该类没有默认构造函数时)
?

初始化列表使用方法:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

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. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A
{
public:
    A(int a)
    :_a1(a)
    ,_a2(_a1)
    {}
    void Print() {
        cout<<_a1<<" "<<_a2<<endl;
    }
private:
    int _a2;
    int _a1;
};

int main()
{
    A aa(1);
    aa.Print();
}

我们成员变量是先声明_a2在声明的是_a1,所以我们在初始化列表时即使我们先写的是_a1,但是因为我们先声明的是_a2,所以我们先用_a1初始化_a2,此时_a2为随机值,因为_a1是随机值,然后我们在用a = 1,来初始化_a1,此时的_a1为1,所以结果为1 和 随机值。

explicit关键词:

explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。

1. 单参构造函数,没有使用explicit修饰,具有类型转换作用,explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译

2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用,explicit修饰构造函数,禁止类型转换

3.也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候,?explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数,?等效于只有一个参数的类构造函数

??

static成员:

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

特性:

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
?

静态成员函数可以调用非静态成员函数吗?不可以。

非静态成员函数可以调用类的静态成员函数吗?可以的

?

常见的面试题:

实现一个类,计算类创建了几个对象。

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;//创建对象0个
	A a1, a2;//默认构造了两次,创建了两个
	A a3(a1);//拷贝构造一次,创建一个
	cout << A::GetACount() << endl;//创建对象3个
}

友元:

友元实际上是一种破坏封装,提供一个外部可以访问内部成员的办法。友元分为:友元函数友元类

友元函数:就是定义在外部的函数,再类中声明时加上friend关键字即可,这样在外部定义的函数也可以访问类中的私有成员,常见的是重载operator<<和operator>>? 这两个场景下使用友元函数,使我们函数可以访问到类的私有,并且我们第一个参数还可以是其他参数,否则我们再类中的函数,第一个参数一定是this指针。

1.友元函数可访问类的私有和保护成员,但不是类的成员函数

2.友元函数不能用const修饰

3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制

4.一个函数可以是多个类的友元函数

5.友元函数的调用与普通函数的调用原理相同

友元类:

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员

友元关系是单向的,不具有交换性。

友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。

友元关系不能继承,在继承位置再详细介绍。

内部类的概念:

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。?

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系。

class A
{
private:
	static int k;
	int h = 111;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//访问静态成员
			cout << a.h << endl;//访问A的私有成员
		}
	};
};

int A::k = 1;
int main()
{
	A::B b;//在A域中的B域中 创建一个b变量
	b.foo(A());

	return 0;
}

我们简单实现下日期类:

1. 我们尽量使用const引用传参数

2.尽量复用代码

3.尽量使函数可以用const变量接收

4.刚开始写的有this指针解引用访问成员,后续是直接写

Date.h头文件的实现

#pragma once
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <assert.h>
using namespace std;

class Date
{
	//友元函数加内联,为了使this指针不占用第一个形参位置
	friend inline ostream& operator<<(ostream& out, const Date& d);
	friend inline istream& operator>>(istream& in,Date& d);

public:
    Date(int year = 1, int month = 1, int day = 1) //全缺省的构造函数
		:_year(year)
		,_month(month) 
		, _day(day)
	{
		assert(CheckDate());
		//cout << "构造完成" << endl;
	}

	Date(const Date& d)//拷贝构造
	{
		assert(CheckDate());
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
		//cout << "浅拷贝" << endl;
	}

	~Date()//析构函数
	{
		//cout << "清理完毕" << endl;
	}
	
	int GetMonthDay(int year, int month)const;//获取每个月的天数。

	bool CheckDate();//检查日期是否合法

	void Print()const;//打印日期
	Date& operator=(const Date& d);//赋值运算符的重载
	bool operator==(const Date& d)const;//判断日期是否相等
	bool operator>(const Date& d) const;//日期是否大于
	bool operator>=(const Date& d) const;//日期是否大于等于
	bool operator<(const Date& d) const;//日期是否小于
	bool operator<=(const Date& d) const;//日期是否小于等于
	bool operator!=(const Date& d)const;//判断日期是否不相等
	Date& operator+=(int day);//日期加等天数
	Date operator+(int day)const;//日期加天数
	Date& operator-=(int day);//日期减等天数
	Date operator-(int day)const;//日期减等天数
	Date& operator++();//日期前置++
	Date operator++(int);//日期后置++
	int operator-(const Date& d)const;//日期相减
	char* GetWeekDay()const;//获取星期几
	
private:
	int _year;
	int _month;
	int _day;
};

//静态的字符数组
static char* WeekDay[7] = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" };
 

//返回值的设定要符合连续的输出
inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	
	return out;
}

inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	assert(d.CheckDate());

	return in;
}

?

Date.cpp函数实现的文件

#include "func.h"

bool Date::CheckDate()//检查日期是否合法
{
	if (_year >= 1
		&& _month > 0 && _month < 13
		&& _day>0 && _day <= GetMonthDay(_year, _month))
	{
		return true;
	}
	else
	{
		return false;
	}
}

int Date::GetMonthDay(int year, int month)const//获取每个月的天数。
{
	//静态数组存放每月天数
	static int MonthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = MonthDay[month];
	if (month == 2
		&& ( (year % 400) == 0 || ((year % 100) != 0 && (year % 4) == 0)))
	{
		day += 1;
	}
	
	return day;
}

void Date::Print()const//打印日期
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}

	return *this;
}

bool Date::operator==(const Date& d)const
{
	if (this->_year == d._year
		&& this->_month == d._month
		&& this->_day == d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

bool Date::operator>(const Date& d) const//日期是否大于
{
	if (_year == d._year
		&& _month == d._month
		&& _day > d._day)
	{
		return true;
	}
	else if (_year == d._year
		&& _month > d._month)
	{
		return true;
	}
	else if (_year > d._year)
	{
		return true;
	}
	else
	{
		return false;
	}
}

bool Date::operator!=(const Date& d)const
{
	return !(*this == d);
}

bool Date::operator>=(const Date& d)const//日期是否大于等于
{
	return (((*this) > d) && ((*this) == d));
}

bool Date::operator<(const Date& d)const//日期是否小于
{
	return !((*this)>d);
}


bool Date::operator<=(const Date& d) const//日期是否小于等于
{
	return  !((*this) > d);
}

Date& Date::operator+=(int day)//日期加等天数
{
	if (day < 0)//天数为负数时,转化为减天数
	{
		return ((*this) -= -day);
	}

    _day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

Date Date::operator+(int day)const//日期加天数
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

Date& Date::operator-=(int day)//日期减等天数
{
	if (day < 0)
	{
		return (*this) += -day;
	}

    _day -= day;

	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

Date Date::operator-(int day)const//日期减等天数
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

Date& Date::operator++()//日期前置++
{
	return (*this) += 1;
}

Date Date::operator++(int)//日期后置++
{
	Date tmp = *this;
	(*this) += 1;
	return tmp;
}

int Date::operator-(const Date& d)const//日期相减
{
	int flag = 1;
	Date max = *this;
	Date min = d;

	if (*this <= d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (max != min)
	{
		min++;
		n++;
	}

	return n*flag;
}


char* Date::GetWeekDay()const//获取星期几
{
	static Date start(1922, 1, 1);
	if (*this < start)
	{
		printf("日期太过久远\n");
		exit(-1);
	}
	int n = (*this) - start;
	int WeekIndex = 6;
	WeekIndex += n;

	return WeekDay[WeekIndex % 7];
}

编译器的优化:

我们编译器对于拷贝构造和默认构造函数,常常会进行优化,在一个表达式中,连续多个构造,可能会被编译器优化成一次,就比如下面的例子:

??

今天就到这里啦!

?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:33:07  更:2022-09-30 00:34:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 5:21:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码