QT应用开发学习(C++进阶知识学习)

这一章非常重要,主要是记录面向对象和面向过程的思想


面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

但是在CSDN等博客上我看到许多人说C++既是面向对象的也是面向过程的,因为它是C语言的扩展,保留了C的面向过程特性,但是扩展了C++的面向特性。我们学习的python和java等都是面向对象的语言


一、类

类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};

上面结构体的定义,在C++中更喜欢用class来代替

类的定义

class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方法

1.声明和定义全部放在类体中
声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理

class Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};

2. 声明放在.h文件中,类的定义放在.cpp文件中

//person.h
class Person
{
public:
   //显示信息
   void show();
public:
   char* _name;
   char* _sex;
   int _age;
}

//person.cpp
#include"person.h>

void Person::show()
{
   cout<<_name<<"  "<<_sex<<"  "<<_age<<endl;
}   

注意:一般情况下我们采用第二种方式

内联函数

刚刚提到了内联函数,现在介绍一下:
内联函数(inline function)是C++中的一种函数形式,它是对编译器的一种建议,用于优化函数调用的开销。内联函数在调用时会直接将函数体的代码插入到调用处,避免了函数调用的开销,从而提高了程序的执行效率。

使用内联函数可以减少函数调用所产生的开销,特别是在频繁调用的小型函数中,尤其明显。内联函数的定义通常放在头文件中,因为编译器需要在调用处进行代码插入。

在C++中,可以使用关键字inline来声明内联函数。以如下方式定义的函数被视为内联函数:

inline return_type function_name(arguments) {
    // 函数体代码
}

需要注意的是,编译器可以选择是否将函数作为内联函数进行处理。当函数体较大、复杂或包含递归调用时,编译器可能会忽略内联的建议,将函数编译为普通函数。

使用内联函数时需要权衡代码的大小和执行效率。适当使用内联函数可以提高程序的性能,但过度使用可能会导致代码体积增大,影响可执行文件的大小。

总之,内联函数是一种用于优化函数调用开销的方式,在C++中通过在函数定义前加上inline关键字来声明。使用内联函数可以避免函数调用的开销,提高程序的执行效率。

类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

在这里插入图片描述
【访问限定符说明】
public修饰的成员在类外可以直接被访问
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
class的默认访问权限为private,struct为public(因为struct要兼容C)

面向对象特性

在类和对象阶段,我们需要知道面向对象的特性:分别是封装 继承 多态

封装本质上是一种管理:我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,用 public封装的成员允许外界对其进行合理的访问。所以封装本质上是一种管理。或者是函数被封装成一个函数接口,供于其他用户使用,这也是封装的体现。

继承是面向对象编程中的一种重要概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类可以继承父类的特征,同时可以添加自己的特定功能。例如,考虑一个"动物"类作为父类,它具有共享的属性和方法,如“名字”和“移动”。而"猫"类和"狗"类可以作为子类继承"动物"类的属性和方法,并且还可以添加自己特定的方法,如"猫"类可以有"抓老鼠"方法,"狗"类可以有"看门"方法。这样,"猫"和"狗"都可以利用"动物"类提供的公共功能,同时还具有各自的特殊功能。

多态是面向对象编程的另一个重要概念,它指的是同一类型的对象在不同情况下表现出不同的行为。多态实现了不同对象对共同操作的不同响应。以前面的例子为例,假设有一个"喂食"方法,这个方法接收一个"动物"类型的对象作为参数,并调用对象的"吃东西"方法。在这里,无论传递给"喂食"方法的是一个"猫"类的实例还是一个"狗"类的实例,它们都可以调用自己特定的"吃东西"方法,实现了多态。

封装、继承和多态是面向对象编程中的关键概念,它们提供了代码重用性、灵活性和可扩展性。通过继承,可以构建类的层次结构,通过多态,可以针对具体对象的不同行为编写通用的代码。

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
 cout<<_name<<" "_gender<<" "<<_age<<endl; 
}

类的实例化

用类类型创建对象的过程,称为类的实例化
类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};


void test()
{
  Person man;   //类的实例化
  man._name="hehe";
  man._age="66";
  man._sex="男";
  man._PrintPersonInfo();
}

类对象模型

如何计算类对象的大小

class A {
public:
 void PrintA()
 {
 cout<<_a<<endl;
 }
private:
 char _a;
};

那么问题来了?类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小? 想要知道这个,首先我们要弄明白类在内存中的存储方式
在这里插入图片描述

那为什么内存要这样存储类了?
原因:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

在这里插入图片描述

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比
较特殊,编译器给了空类一个字节来唯一标识这个类

类的6个默认成员函数

如果一个类中什么成员都没有,我们简称其为空类。但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。

class Date {}; //空类

在这里插入图片描述

构造函数

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


例如,上述日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。(这儿可以先暂时这么理解

构造函数的特性

函数名与类名相同。
无返回值。
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载。

class Date
{
public:
     // 1.无参构造函数
     Date ()
     {}
	Date(int year = 0, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

void TestDate()
{
 Date d1; // 调用无参构造函数
 Date d2 (2015, 1, 1); // 调用带参的构造函数
 
 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
 Date d3(); 
}

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

class Date
{
public:
 /*
 // 如果用户显式定义了构造函数,编译器将不再生成
 Date (int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 */
private:
 int _year;
 int _month;
 int _day;
};

void Test()
{
  // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
   Date d; 
}

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

// 默认构造函数
class Date
{ 
public:
 Date()
 {
 _year = 1900 ;
 _month = 1 ;
 _day = 1;
 }
 
 Date (int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ;
 int _month ;
 int _day ;
};
// 以下测试函数能通过编译吗?
void Test()
{
 Date d1; 
}

像这种情况可以编译成功吗?(显然这儿是过不了的,因为类中有多个默认函数。)
在这里插入图片描述
编译器对内置类型使用默认构造函数时,对其成员赋的是随机值。但对自定义类型,会调用它的默认函数
在这里插入图片描述
这儿并没有我们自己写的构造函数,所以编译时会调用默认的构造函数,又由于类成员都是内置类型,因此赋的都是随机值。下面我们再来看看自定义类型。
在这里插入图片描述

注意:如果你Time类中没有自己写构造函数,用编译器默认的构造函数,它也是一样会输入随机值的。

析构函数

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

特性
析构函数名是在类名前加上字符 ~。
无参数无返回值。
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
编译器自动生成的析构函数机制:
 1、编译器自动生成的析构函数对内置类型不做处理。
 2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。
对象生命周期结束时,C++编译系统系统自动调用析构函数。
先构造的后析构,后构造的先析构
在这里插入图片描述

拷贝构造函数

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

特性
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)// 拷贝构造函数 ,与构造函数形成函数重载
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2021, 9, 27);
	Date d2(d1); // 用已存在的对象d1创建对象d2

	return 0;
}


在这里插入图片描述
因此通过形参不写成引用的形式,会形成无限递归。

若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

一般涉及到堆区的问题,浅拷贝是无法解决问题的

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_ps = (int*)malloc(sizeof(int)* capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		cout << _ps << endl;// 打印栈空间地址
	}
private:
	int* _ps;
	int _size;
	int _capacity;
};

int main()
{
	Stack s1;
	s1.Print();// 打印s1栈空间的地址
	Stack s2(s1);// 用已存在的对象s1创建对象s2
	s2.Print();// 打印s2栈空间的地址
	return 0;
}

我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。这段代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。

在这里插入图片描述

结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈。

在这里插入图片描述
这个时候问题就很严重了。首先我们对s1的修改都会直接影响s2,而且更重要的一个是:我们对它们共同指向的那块空间进行了两次的析构,会造成空间多次释放的问题。

二、this指针

this指针的引出

定义一个日期类

class Date
{ 
public :
 void Display ()
 {
 cout <<_year<< "-" <<_month << "-"<< _day <<endl;
 }
 
 void SetDate(int year , int month , int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};


int main()
{
 Date d1, d2;
 d1.SetDate(2018,5,1);
 d2.SetDate(2018,7,1);
 d1.Display();
 d2.Display();
 return 0; 
 }

对于上述类,有这样的一个问题:
Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

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

this指针的特性

this指针的类型:类型* const 只能在“成员函数”的内部使用
this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
在这里插入图片描述

三、运算符重载

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

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
5.
、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现
*

==运算符重载

bool operator==(const Date& d1, const Date& d2) {
 return d1._year == d2._year;
 && d1._month == d2._month
 && d1._day == d2._day; }

对于这个重载的函数,你可以定义再类里面,这样就少一个参数,因为有this指针的存在。你也可以定义在外面,但是定义在外面时,可能你的类成员时private封装的,无法访问到,这时有两个解决办法:一是把类成员用public封装,二是用友元函数(之后会讲到)。

= 运算符重载

	Date& operator=(const Date& d)// 赋值运算符重载函数
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

这里为什么要返回引用了?如果你去测试发现D1=D2,如果你的返回值是Date的话,似乎也能过,但是如果你的测试用例是D1=D2=D3的话,那就一定过不了了,因为你不是返回的对象本身,无法形成链式编程,这也是为什么这儿返回*this的原因,因为this是指向左操作符的。这样就可以一直赋值了。

重载赋值运算符需要注意以下几点
一、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。

二、返回值使用引用返回
原因在=运算符重载中说过了,为了返回对象自身,形成链式编程。(return *this才是返回自身,不要忘记解引用哦)

三、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
但是有些类就不行了,所以有些类还是要我们自己写赋值运算符重载的。
注意区分拷贝和赋值:

	Date d1(2021, 6, 1);
	Date d2(d1);
	Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const修饰成员函数

我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象

	void Print()const// cosnt修饰的打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}


在使用const时要注意,权限不能放大,但是可以缩小。

虚函数和纯虚函数

虚函数(impure virtual)

C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。子类可以重写父类的虚函数实现子类的特殊化

如下就是一个父类中的虚函数:

class A
{
public:
     virtual void ss()
    {
        cout<<"我是基类的虚函数"<<endl;
    }
};

纯虚函数(pure virtual)

C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
  C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
  C++中的纯虚函数也是一种“运行时多态”。
  如下面的类包含纯虚函数,就是“抽象类”

class A
{
public:
    virtual void out1(string s)=0;  //我是基类的虚函数 
};

例如:

#include<iostream>
#include<string>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
class a
{
	private:
	
	public:
		a(){      //构造函数用内联函数的形式 
			
		}
		//虚函数 
		virtual  void  xhs(){   	   //这个虚函数必须得在基类中实现 
			cout<<"我是基类的虚函数"<<endl;//即使是空的虚函数也要在基类中实现 
		}  //派生类中可以不写这个函数,但是派生类对象调用时会调用积累的虚函数 
		
		//纯虚函数 
		virtual void cxhs() =0;  //这个纯虚函数不在基类中实现,必须在子类中实现 
		
}; 
class b:public a
{
	private:
		
    public:
    	void xhs(){         //这个是可有可无的 
    		cout<<"我是派生类覆盖基类虚函数的函数"<<endl; 
		}                        //*
		                         //*
		void cxhs(){         //这个是必须有实现的 
			cout<<"我是派生类覆盖基类虚函数的函数"<<endl; 
		}       //*              //*
}; 				//* 			 //*
int main()      //*              //*
{               //*              //*
	b c;        //*              //* 
	c.xhs();    //*           //调用派生类的 
	c.cxhs();//调用派生类的 
}

总的来说:虚函数就是继承中子类可以覆盖父类所写的函数,纯虚函数就是函数内部没有实现语句。

总结

以上就大部分是我们所需要学习的c++知识了,当然还有友元函数那些,需要的自己查询吧。有了这些知识已经可以开始学习QT了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值