C#入门篇章—Class类专题
class(类),是面向对象中的概念,是面向对象编程的基础。类是对现实生活中一类具有共同特征的事务的抽象,用来清晰管理你的行为。
class的实例化:定义一个类后,就必须实例化才能使用。实例化就是创建一个对象的过程。在C#中,使用new关键字来创建。
类 对象 = new 类 () ;
类的声明是以关键字class开始,后跟类的名称组成的。
类的实例是以关键字new开始,后跟类的名称组成的。
在类内声明的非静态变量,称之为 普通成员变量。
在类内声明的变量之前追加static关键字,称之为 静态成员变量。
在类内声明的非静态函数,称之为 普通成员函数。
在类内声明的函数之前追加static关键字,称之为 静态成员函数。
在类的内部声明的静态变量或函数,若想访问,必须通过类名来访问;普通成员变量或函数通过实例化的对象来访问。
构造函数:
public 类名()
{
……
}
析构函数:
~类名()
{
……
}
所谓的访问修饰符,指的是 是否能够访问的到。
- Public:任何公有成员可以被外部的类访问。
- Private:只有同一个类的函数可以访问它的私有成员。
- Protected:该类的内部和继承类可以访问。
- internal:同一个程序集(命名空间)的对象可以访问。
- Protected internal:3和4的并集,符合任意一条可以访问。
范围比较:
private < internal/protected < protected internal < public
protected限定的是只有在继承的子类中才可以访问,可以跨程序集 ;
internal限定的是只有在同一个程序集中才可以访问,可以跨类 。
继承是面向对象程序设计中最重要的概念之一,继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得容易。同时也有利于重用代码和节省开发时间。
当创建一个类时,不需要完全重写新的成员变量和成员函数,只需要设计一个新的类,继承已有的类的成员即可。这个已有的类被称为 基类,这个新的类被称为 派生类。
C#中用“ : ”表示继承。C#不支持多重继承。
继承,就是将 共用的属性或方法抽离到基类 的过程,这个思维称之为面向对象。
封装,被定义为“把一个或多个项目封闭在一个物理的或逻辑的包中”。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。
封装,是将实现细节通过接口的方式暴露给第三方,而不需要关心实现细节。
封装和抽象是相辅相成的,抽象允许相关信息可视化,而封装则是使开发者实现所需级别的抽象。
C#的封装根据具体的需要,设置使用者的权限,并通过 访问修饰符 来实现。
访问修饰符
- Public:任何公有成员可以被外部的类访问。
- Private:只有同一个类的函数可以访问它的私有成员。
- Protected:该类的内部和继承类可以访问。
- internal:同一个程序集(命名空间)的对象可以访问。
- Protected internal:3和4的并集,符合任意一条可以访问。
范围比较:
private < internal/protected < protected internal < public
virtual代表虚函数,意味着 子类可以覆盖其实现,如果子类不覆盖,那将使用父类的同名函数。
子类使用override重写虚函数。
多态,是同一个行为具有多个不同表现形式或形态的能力;多态,就是同一个接口,使用不同的实例而执行不同操作。
- 静态多态(编译时)在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C#提供了两种技术来实现静态多态性。分别为:函数重载和运算符重载。
- 动态多态(运行时)在运行时,根据实例对象,执行同一个函数的不同行为。运行时多态,在运行前无法确认调用哪个方法,只有在运行时才能确定的方法,这种行为称之为动态多态。具体实现为,将派生类实例化对象赋给基类实例化的对象,用后者调用继承的方法。// 注意: 只能子类给父类对象赋值,不能反过来Rectangle rectangle = new Rectangle();Triangle triangle = new Triangle();Polygon baseParent1 = rectangle;Polygon baseParent2 = triangle;baseParent1.Show(); // 结果为rectangle showbaseParent2.Show(); // 结果为triangle show
覆盖,发生在继承关系中,通过virtual和override实现,函数名和函数参数一模一样;
重载,发生在任何关系中,只要保证函数名字一致,参数不一致(带参数和不带参数,带参数顺序不一致,或者参数个数不一致),即可实现重载。
this:可访问当前类能访问到的属性和方法;
base:只能访问基类的属性和方法。
类可以声明为static,这将变成一个静态类,不得被继承,特点是仅包含静态成员或常量。
静态类的特点:
- 不能被实例化
- ,意味着 不能使用new关键字创建静态类的实例;
- 仅包含静态成员或常量
- ;
- 不能被继承
- ;
- 不能包含实例构造函数,但可以包含静态构造函数
- ;
- 静态构造函数不可被调用
- 。
静态类一般用于 工具类。
补充知识点:
- const在类内声明的const常量
- (1)外部访问时,必须通过类名进行访问;
- (2)只能在声明时初始化,不允许在任何其他地方对其初始化(包括构造函数);
- (3)在某种程度上,被const修饰的变量(常量)为不可变值。
- readonly在类内声明的readonly常量
- (1)readonly const不可共同修饰一个数据类型(基本数据量类型 + 自定义数据类型);
- (2)readonly修饰的类型,可以被类的实例进行访问,但不可修改值;
- (3)readonly的初始化只能发生在构造函数或声明中。
- 变量访问修饰符的控制
- 可通过访问修饰符构成的语法块,来实现类似 外部只读的效果。get set 以及学习 value赋值和访问代码的执行流程public int mValue3 {get; private set;}
- 静态构造函数
- (1)静态构造函数不需要增加访问修饰符;
- (2)静态构造函数无论多少实例,都只被系统自动调用一次。
- 静态类:
- (1)静态类不允许有实例构造函数,也不能有析构函数,只允许存在一个静态构造函数(静态类的静态构造函数不会执行);
- (2)静态类不允许被实例化;
- (3)静态类中的成员必须是 静态成员或常量;
- (4)静态类无法作为基类派生。
类可以被声明为sealed,这将变成为一个密封类。
密封类的特点:
- 不能被继承,但可以继承别的类或接口;
- 密封类不能声明为抽象类;
- 密封类内的成员函数,不能声明为sealed
密封类一般用于 防止重写某些类或接口影响功能的稳定。
密封类:
- 不允许被继承
- sealed和abstract无法共存;
- 密封类内的函数,不允许增加sealed关键字;
- 密封类 可以正常继承 常见类(普通类、抽象类)接口;
类可以被声明为abstract,这将变成一个抽象类。
抽象类的特点:
- 不能被实例化,意味着 不能使用new关键字创建实例;
- 可只提供部分函数实现,也可仅声明抽象函数。
抽象类一般用在什么地方?
抽象类是提炼出了一些类共有的属性或函数接口的组织,为子类提供设计思路,配合多态多用于代码架构设计。
抽象类 1. 不允许实例化 2. 支持构造函数 3. 抽象类可继承抽象类 4. 静态构造函数只执行一次,但是其他的构造函数则根据不同实例,分别再次调用 5. 允许存在 virtual 虚函数 6. 若函数声明为abstract,则不允许包含函数体,子类必须显式覆盖父类的该方法。
类名后可以添加<T1, T2, T3……>,这将变成一个泛型类。泛型T1,T2,T3可以通过where关键字 来限定类型。
泛型类的特点:
- 在声明时可以不指定具体类型,但是在new实例化时必须指定T类型;
- 可指定泛型类型约束;
- 如果子类也是泛型,那么继承的时候可以不指定具体类型。
泛型类一般用于 处理一组功能一样,但类型不同的任务。
interface + name,这将变成一个接口。
接口的特点:
- 接口只声明接口函数,不能包含实现;
- 接口函数访问修饰符,必须是public,默认也就是public;
- 接口成员函数的定义是派生类的责任,接口提供了派生类应遵循的标准结构;
- 接口不可被实例化
- ,即不可 new ;
- 接口可继承其他接口,可进行多继承;
- 一个类不能继承多个类,但是可以继承多个接口。
接口一般用在 约束一些行为规范时 。
C#中一个类继承接口,必须实现接口中定义的函数方法,实现方法可分为隐式实现和显式实现:
显式和隐式实现接口的区别在于:
对于隐式实现的成员,既可以通过类对象实例来访问,也可以通过接口来访问;而对于显式实现的对象,只能通过接口来访问,不能使用类对象来访问。
相同点:
- 都可以被继承;
- 都不能被实例化;
- 都可以包含方法声明;
- 派生类必须实现未实现的方法。
不同点:
- 抽象类可以定义字段、属性、方法实现;接口只能方法声明,不能包含字段;
- 接口不允许有构造函数(包括普通构造函数和静态构造函数),而抽象类可以;
- 接口不允许有函数实现,而抽象类可以;
- 抽象类只能被单一继承;接口可以多重继承;
- 抽象类更多的是定义一系列紧密相关的类间;而接口大多数是关系疏松但都实现某一功能的类中。
- 函数的访问修饰符,接口默认为public,不允许改变为private;抽象类默认private,函数前若是abstract,那访问修饰符也不能是private,但是非abstract声明的函数 是允许private protected的。
用 struct + name {…………} 声明一个结构体。
结构体是值类型数据结构,它使得一个单一变量可以存储各种数据类型的相关数据。
结构体的特点:
- 结构体可带有方法、字段、索引、属性、运算符方法和事件;
- 结构体可定义构造函数,但不能定义析构函数、不能定义无参构造函数。无参构造函数是自动定义的,且不能被改变;
- 与类不同,结构体不能继承其他结构体或类,但是可以实现一个或多个接口;
- 结构体不能作为其他结构体或类的基础结构;
- 结构成员不能指定为abstract、virtual 或 protected;
- 结构体不用通过 new 来实例化。
struct和class的异同
相同点 1. 都支持静态构造函数、有参构造函数; 2. 都支持自定义函数; 3. 结构体和类对于const修饰的变量的使用方式是一样的。
不同点 1. 构造函数:结构体 不允许定义无参构造函数,只允许定义有参构造函数,但是类可以; 2. 析构函数:结构体不允许定义析构函数,但类可以; 3. 函数修饰符:结构体函数不允许声明为virtual、protected,但是类可以; 4. 类型修饰符:结构体类型不允许声明为abstract,但是类可以; 5. 关于变量 (1)普通变量 结构体声明的全局普通变量(不带修饰符),不能在声明时直接赋值,只能在构造函数里赋值,但是类都可以; (2)readonly类型的变量 结构体声明的全局readonly变量,只能在构造函数里赋值,而类都可以。 6. 关于继承 结构体之间不可以互相继承,但是类与类之间是可以继承的(sealed密封类除外)。 7. 在使用上
(1)访问变量 结构体访问成员变量,给变量显式赋值,就可直接访问;而类必须实例化后才能访问; 结构体如果不通过new初始化,是不可以直接访问其内部变量的(const除外)。 (2)访问函数 结构体变量和类对象 必须进行初始化,才可以访问
- new
- (1)结构体属于值类型,结构体的new,并不会在堆上分配内存,仅仅是调用结构体的构造函数初始化而已; (2)类属于引用类型,类的new,会在堆上分配内存,而且也会调用类的构造函数。
is和as就是为了解决 强制类型转换可能导致异常 的问题而使用的。
is:检查对象类型的兼容性,并返回结果true(false)
as:检查对象类型的兼容性,并返回转换结果,如果不兼容则返回null
is和as的区别在于:
- 使用 as 更加安全,使用 as 如果转换失败,返回 Null ,不会抛异常;
- 使用 as 效率会更高。使用 is 时会检查两次对象的类型,一次是核实,一次是强制类型转换,而使用 as 只进行了一次对象类型的检查。
强制(显式)类型转换。形式通过(type)a来表示,一般用于 高精度数据类型转换为低精度数据类型。
隐式类型转换。发生在 低精度数据类型自动转换为高精度数据类型。
前置知识:
引用类型: string、数组、类、接口
值类型: (s)byte、(u)short、(u)int、(u)long、bool、enum、struct
is:可检测 值类型和引用类型,成功返回true,否则返回false。
as:首先会判断 源数据类型 是否是 目标数据类型,不是的话编译器会报错。as转换成功,返回源数据类型存储的数据,否则返回空。用于检测引用类型。
类型:
- 值类型
- 内置值类型
- 用于定义的值类型
- 枚举类型
- 引用类型
- 指针类型
- 接口类型
- 自描述类型
- 数组
- 类类型
- 用户定义的类
- 已装箱的值类型
- 委托
装箱:值类型转换为引用类型;
拆箱:引用类型转换为值类型。
- 值类型只需要一段单独的内存,用于存储实际的数据(单独定义时放在栈中)。
- 引用类型需要两段内存:
a. 第一段存储实际的数据,它总是位于堆中;
b. 第二段是一个引用,指向数据在堆中的存放位置。
- 值类型与引用类型的存储方式
- 值类型:值类型总是分配在它声明的地方,作为局部变量时,存储在栈上;作为类对象的字段时,则跟随此类存储在堆中。
- 引用类型:引用类型存储在堆中。类型实例化时,会在堆中开辟一部分空间存储类的实例。类对象的引用还是存储在栈中。
- 案例// 值类型,保存在栈中int num = 100;// 引用类型,保存在堆中int[] nums = {1, 2, 3, 4, 5};// 输出Console.WriteLine(num); // 输出结果:100Console.WriteLine(nums); // 输出结果:System.Int32[]// 案例表明,num为值类型,直接输出了值,而nums为引用类型,输出了一个引用
- 值类型与引用类型的区别
- 值类型和引用类型都继承自 System.Object 类。不同之处在于,几乎所有引用类型都是直接从System.Object继承,而值类型则是继承System.Object的子类System.ValueType类。
- 在给引用类型的变量赋值时,其实只是赋值了对象的引用;而给值类型变量赋值时,是创建了一个副本。
C#程序在CLR上运行时,内存从逻辑上划分两大块:栈、堆。这两个基本元素组成了C#程序的运行环境。
- 堆与栈堆:在C语言中叫堆,在C#中其实叫托管堆;栈:即堆栈,因为和堆一起叫别扭,所以简称为栈。
- 托管堆托管堆不同于堆,它是由CLR(公共语言运行库Common Language Runtime)管理,当堆中满了之后,会自动清理堆中的垃圾。所以,做 .NET 开发,不需要关心内存释放的问题。
- 内存堆栈和数据堆栈
- 内存堆栈:存在内存中的两个存储区(堆区,栈区)。
- 栈区:存放函数的参数、局部变量、返回数据等值,由编译器自动释放。
- 堆区:存放着引用类型的对象,由CLR释放。
- 数据堆栈:是一种后进先出的数据结构,它是一个概念,主要是栈区。
栈通常保存着我们代码执行的步骤,如一个值类型的变量的初始化或者一个方法的声明。而堆上存放的多是对象、数据等。
我们可以将栈想象成一个接一个叠放在一起的盒子。当我们使用时,每次从最顶部取走一个盒子。同样我们的栈就是如此,当一个方法(或类型)被调用完成时,就从栈顶取走,接着下一个,这就是所谓的“先进后出”。
堆则不然,堆更像是一个仓库,储存的是我们使用的各种对象等信息,当我们需要调用时,会去里面自行寻找并调用。跟栈不同的是它们被调用完毕后不会立即被清理掉。
注意:栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕时,立马释放。而堆则需要GC(Garbage Collection垃圾回收)清理。
我们把内存分为堆空间和栈空间,区别如下:
- 栈空间比较小,但是读取速度快;
- 堆空间比较大,但是读取速度慢。
1、栈的深入讲解
栈(stack)最明显的特征就是“先进后出”,本质上来讲堆栈也是一种线性结构,符合线性结构的基本特点:即每个节点有且只有一个前驱节点和一个后续节点。栈把所有操作限制在“只能在线性结构的某一端”进行,而不能在中间插入或删除元素。我们把数据放入栈顶称为入栈(push),从栈顶删除数据称为出栈(pop)。
2、堆的深入讲解
堆(Heap)是一块内存区域,与栈不同,堆里的内存能够以任意顺序存入和移除。
CLR 的 GC 就是内存管理机制,我们写程序时不需要关心内存的使用,因为这些都是CLR帮我们做了。
结果:
原因在于test01和test02在栈中的引用指向的是同一个元素:test[0],而数组test[]作为引用类型,存储的元素为类对象,同样为引用类型,指向的是同一个name,故两次修改同一个值,输出为王五。
怎样才算学会了C++基础,一篇文章学习了解(包含Qt内容)
C++的基础语法包括以下几个方面:
- 注释
C++支持单行注释(以“//”开头)和多行注释(以“/”开头,“/”结尾)。
- 标识符
标识符是指变量、函数、类、结构体等的名称。标识符必须以字母或下划线开头,后面可以是字母、数字或下划线。C++对大小写敏感。
- 关键字
C++有一些关键字,这些关键字具有特殊的含义,不能用作标识符。例如:int、double、if、else、for、while等。
- 数据类型
C++有基本数据类型和用户自定义数据类型。基本数据类型包括整型、浮点型、字符型、布尔型等。用户自定义数据类型包括结构体、枚举、类等。
- 变量
变量是指用来存储数据的内存位置。定义变量时需要指定变量的数据类型和名称。变量的值可以在程序运行过程中被修改。
- 常量
常量是指不能被修改的值。C++有字面常量和符号常量。字面常量是指直接在代码中使用的常量值,例如:10、3.14、\’A\’等。符号常量是指用#define或const关键字定义的常量,例如:#define PI 3.14、const int MAX_NUM = 100等。
- 运算符
C++支持算术运算符、关系运算符、逻辑运算符、位运算符等。例如:+、-、*、/、%、==、!=、<、>、&&、||、&、|等。
- 控制语句
C++有选择结构和循环结构两种控制语句。选择结构包括if语句和switch语句,循环结构包括while语句、do-while语句和for语句。这些控制语句可以根据条件执行不同的代码块,从而控制程序的执行流程。
- 函数
预处理:预定义宏 编译:编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 汇编:汇编语言代码翻译成目标机器指令的过程 连接:链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
C++的内存分区主要包括以下几个部分:
- 栈区(Stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其特点是先进后出。
- 堆区(Heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收,其特点是先进先出。
- 全局区(Static):存放全局变量和静态变量,包括初始化的和未初始化的,程序结束后由操作系统回收。
- 常量区(Const):存放常量,如字符串常量等,程序结束后由操作系统回收。
- 代码区(Code):存放程序的代码,由操作系统分配和回收,不允许写入。
在程序运行过程中,栈区、堆区和全局区的大小可以动态变化,而常量区和代码区的大小是固定的。程序员在使用内存时,需要注意内存分配和释放,以避免内存泄漏和内存溢出等问题。
- 栈区:函数中的局部变量,比如:
- 堆区:动态分配的内存,比如:
- 全局区:全局变量和静态变量,比如:
- 常量区:常量字符串,比如:
- 代码区:程序的代码,比如:
C++中,引用和指针都可以用来间接访问变量,但它们之间有以下区别:
- 引用必须在定义时被初始化,而指针可以先定义再赋值。
- 引用不允许指向空,而指针可以指向空。
- 引用在使用时不需要解引用操作,而指针需要使用星号(*)操作符来解引用。
下面是一个例子,演示了引用和指针的使用:
输出结果:
在函数参数中,引用和指针都可以用来传递参数,但它们之间也有一些区别:
- 引用作为函数参数时,传递的是实参的别名,函数内部对引用的修改会直接影响到实参的值。而指针作为函数参数时,传递的是实参的地址,需要使用解引用操作符(*)来访问实参的值,函数内部对指针的修改不会影响到实参的地址,但可以通过指针解引用修改实参的值。
- 引用作为函数参数时,不需要使用取地址符(&)来传递参数,而指针作为函数参数时需要使用取地址符(&)来传递参数。
- 引用作为函数参数时,不能为null,而指针作为函数参数时可以为null。
下面是一个例子,演示了引用和指针作为函数参数的使用:
输出结果:
可以看到,通过引用和指针都可以修改实参的值,但引用更加简洁,不需要使用解引用操作符(*),并且可以避免空指针的问题。但指针可以指向null,有时候也是很有用的。
C++的构造函数和析构函数是特殊的成员函数,用于在对象创建和销毁时执行特定的操作。构造函数用于初始化对象的成员变量,而析构函数用于释放对象占用的资源。
以下是一个示例代码:
注意事项:
- 构造函数和析构函数的名称与类名相同,且没有返回类型。
- 构造函数可以有参数,用于初始化成员变量,也可以没有参数。
- 如果没有定义构造函数,则编译器会生成默认的构造函数。
- 如果一个类定义了析构函数,则编译器不会生成默认的析构函数。
- 构造函数和析构函数的访问权限可以是public、protected或private。
- 构造函数和析构函数不能被继承。
- 在构造函数中,可以使用初始化列表来初始化成员变量,这比在构造函数体中赋值更高效。
- 在析构函数中,应该释放对象占用的资源,如堆内存、文件句柄等。
C++的三大特征是:
- 封装
封装是将数据和操作数据的方法(函数)组合在一起,形成一个类。类将数据和方法封装在一起,使得数据不能被外部直接访问和修改,只能通过类的公共接口进行访问和修改。这样可以保证数据的安全性和可靠性,同时也提高了代码的可维护性和可扩展性。
下面举一个例子来说明封装的概念。假设我们要写一个银行账户管理系统,每个账户包括账户名称、账户号码、账户余额等信息。我们可以定义一个Account类来封装这些信息,具体实现如下:
在上面的代码中,我们将数据成员(name、number、balance)声明为私有的,外部代码无法直接访问和修改这些数据。同时,我们提供了公共成员函数(getName、getNumber、getBalance、deposit、withdraw)来访问和修改这些数据。这样,外部代码只能通过公共接口来操作数据,从而保证了数据的安全性和可靠性。
例如,我们可以通过以下代码创建一个账户对象,并进行存款和取款操作:
通过封装,我们可以将数据和操作数据的方法封装在一起,提高了代码的可维护性和可扩展性,同时也保证了数据的安全性和可靠性。
- 继承
继承是指一个类可以从另一个类中继承属性和方法。被继承的类称为基类或父类,继承的类称为派生类或子类。子类可以重写父类的方法,也可以添加新的属性和方法。继承可以提高代码的重用性和扩展性,同时也可以实现多态性。
C++继承是面向对象编程中的一种重要概念,它允许一个类继承另一个类的属性和方法。C++支持单继承和多继承两种方式。
单继承:一个派生类只能继承一个基类的属性和方法。语法格式如下:
继承方式可以是public、protected或private,分别表示公有继承、保护继承和私有继承。
下面是一个简单的单继承示例:
在这个示例中,Animal是基类,Cat是派生类。Cat继承了Animal的属性和方法,并且添加了自己的方法meow。创建Cat的实例后,可以调用父类的方法eat和子类自己的方法meow。
多继承:一个派生类可以继承多个基类的属性和方法。语法格式如下:
下面是一个简单的多继承示例:
在这个示例中,Bird继承了Animal和Flyable两个基类的属性和方法,并且添加了自己的方法chirp。创建Bird的实例后,可以调用父类的方法eat和fly以及子类自己的方法chirp。
C++继承还涉及到虚函数、重载、多态等高级特性,这些特性可以更好地组织和管理复杂的程序。
- 多态
多态是指同一种类型的对象,在不同的情况下表现出不同的行为。C++实现多态的方式有两种:虚函数和模板。虚函数是指在基类中定义一个虚函数,在派生类中可以重写该函数,通过基类指针或引用调用该函数时,会根据指向的对象类型来调用相应的函数。模板是指定义一个通用的函数或类,可以接受不同类型的参数,根据参数类型的不同,会生成不同的函数或类,从而实现多态。多态可以提高代码的灵活性和可扩展性。
C++多态是面向对象编程中的一种重要概念,它允许不同的对象对同一个消息作出不同的响应。多态可以提高代码的可维护性和可扩展性,使得程序更加灵活。
C++多态实现的基础是虚函数(Virtual Function),虚函数是在基类中声明的函数,在派生类中可以被重写(Override)。通过在基类中将函数声明为虚函数,可以使得派生类中的同名函数自动成为虚函数,并且可以被动态绑定(Dynamic Binding)。
下面是一个简单的多态示例:
在这个示例中,Animal是基类,Cat和Dog是派生类。Animal中的speak函数被声明为虚函数,Cat和Dog重写了speak函数。创建Animal的指针指向Cat和Dog的实例后,调用speak函数会根据实际指向的对象来动态绑定,从而实现了多态。
C++多态还可以通过抽象类(Abstract Class)和纯虚函数(Pure Virtual Function)实现。抽象类是不能被实例化的类,它包含至少一个纯虚函数,纯虚函数是没有实现的虚函数,派生类必须重写纯虚函数才能被实例化。抽象类和纯虚函数可以强制规定派生类必须实现的接口,从而使得程序更加健壮和可靠。
C++中的模板是一种通用的编程工具,它允许我们编写可重用的代码,以适应不同类型的数据。模板可以定义类模板和函数模板,其中类模板用于定义通用的数据类型,而函数模板用于定义通用的函数。
类模板示例:
在此示例中,我们定义了一个名为myVector的类模板,它使用类型T作为参数。该类有一个私有数据成员data,它是一个指向T类型的指针,还有一个size变量,用于存储向量的大小。该类还定义了一个构造函数和一个运算符[],用于访问向量中的元素。
函数模板示例:
在此示例中,我们定义了一个名为maximum的函数模板,它使用类型T作为参数。该函数接受两个参数x和y,它们必须是相同类型的。该函数比较x和y的值,并返回较大的那个。
使用模板的示例:
在此示例中,我们首先创建了一个myVector对象v,它存储了5个整数。然后,我们使用maximum函数模板来比较不同类型的值,例如整数、浮点数和字符。这些值都是模板参数T的实例化。
C++11是C++语言的一个重要版本,引入了许多新特性,包括语言特性、标准库特性等。下面是C++11的一些新特性:
- 自动类型推导(auto关键字)
C++11引入了auto关键字,可以自动推导变量的类型:
- 基于范围的for循环(range-based for循环)
C++11引入了基于范围的for循环,可以方便地遍历容器中的元素:
- nullptr关键字
C++11引入了nullptr关键字,可以代替NULL指针常量:
- 右值引用(rvalue reference)
C++11引入了右值引用,可以绑定到右值表达式上:
右值引用可以用于实现移动语义和完美转发。
- 移动语义(move semantics)
C++11引入了移动语义,可以将资源所有权从一个对象转移到另一个对象,避免不必要的复制操作:
- lambda表达式
C++11引入了lambda表达式,可以方便地定义匿名函数:
- constexpr关键字
C++11引入了constexpr关键字,可以在编译时求值:
- 智能指针(smart pointers)
C++11引入了智能指针,可以自动管理资源的生命周期,避免内存泄漏:
- 回调函数
- thread多线程与共享内存&互斥量mutex
STL(Standard Template Library)是C++标准库的一个重要组成部分,它提供了一组通用的模板类和函数,用于实现各种常用的数据结构和算法,如容器、迭代器、算法和函数对象等,使得C++程序员可以更加方便地进行编程。
STL库的主要组成部分包括以下几个方面:
- 容器(Containers):STL提供了多种容器,如vector、list、deque、set、map等,用于存储和管理不同类型的数据。这些容器实现了各种不同的数据结构,如数组、链表、树等,提供了丰富的接口和算法,方便快捷地进行数据操作。
- 迭代器(Iterators):STL提供了多种迭代器,如输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器等,用于遍历容器中的元素。迭代器提供了一种统一的接口,使得算法和容器之间可以进行无缝协作。
- 算法(Algorithms):STL提供了大量的算法,如排序、查找、拷贝、变换等,用于对容器中的元素进行处理。这些算法实现了各种常用的操作,如查找最大值、计算总和、去重复等,可以大大提高程序的效率和可读性。
- 函数对象(Function Objects):STL提供了多种函数对象,如一元函数对象、二元函数对象、谓词等,用于对容器中的元素进行操作。函数对象提供了一种方便的方式,使得程序员可以自定义算法和操作,从而更好地适应不同的需求。
STL库的使用可以大大提高C++程序的效率和可读性,同时也为程序员提供了更多的便利。在实际编程中,程序员可以根据不同的需求,选择合适的容器、迭代器、算法和函数对象,从而实现高效的数据操作和算法实现。
- 饿汉模式:
在程序启动时就创建单例对象,因此也被称为“饱汉模式”。线程安全性较好,但是可能会浪费资源。
- 懒汉模式:
只有当需要使用单例对象时才进行创建,因此也被称为“懒汉模式”。需要考虑线程安全问题,否则可能会导致多个线程同时创建单例对象。
线程安全问题可以通过加锁实现,例如使用std::mutex:
- 数组(Array):一组连续的内存单元,用于存储同种类型的数据。
- 链表(Linked List):一组不连续的内存单元,用指针连接起来,可以动态地添加或删除元素。
- 栈(Stack):一种后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。
- 队列(Queue):一种先进先出(FIFO)的数据结构,只能在队尾进行插入操作,在队头进行删除操作。
- 树(Tree):一种非线性数据结构,由节点和边组成,每个节点可以有多个子节点。
- 图(Graph):一种非线性数据结构,由节点和边组成,每个节点可以有多个相邻节点。
- 堆(Heap):一种特殊的树形数据结构,常用于实现优先队列。
- 哈希表(Hash Table):一种通过散列函数将键映射到值的数据结构,可以实现高效的查找和插入操作。
稳定,平均/最差时间复杂度 O(n²),元素基本有序时最好时间复杂度 O(n),空间复杂度 O(1)。
每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
直接插入没有利用到要插入的序列已有序的特点,插入第 i 个元素时可以通过二分查找找到插入位置 insertIndex,再把 i~insertIndex 之间的所有元素后移一位,把第 i 个元素放在插入位置上。
又称缩小增量排序,是对直接插入排序的改进,不稳定,平均时间复杂度 O(n^1.3^),最差时间复杂度 O(n²),最好时间复杂度 O(n),空间复杂度 O(1)。
把记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减小增量,当增量减至 1 时排序完毕。
不稳定,时间复杂度 O(n²),空间复杂度 O(1)。
每次在未排序序列中找到最小元素,和未排序序列的第一个元素交换位置,再在剩余未排序序列中重复该操作直到所有元素排序完毕。
是对直接选择排序的改进,不稳定,时间复杂度 O(nlogn),空间复杂度 O(1)。
将待排序记录看作完全二叉树,可以建立大根堆或小根堆,大根堆中每个节点的值都不小于它的子节点值,小根堆中每个节点的值都不大于它的子节点值。
以大根堆为例,在建堆时首先将最后一个节点作为当前节点,如果当前节点存在父节点且值大于父节点,就将当前节点和父节点交换。在移除时首先暂存根节点的值,然后用最后一个节点代替根节点并作为当前节点,如果当前节点存在子节点且值小于子节点,就将其与值较大的子节点进行交换,调整完堆后返回暂存的值。
稳定,平均/最坏时间复杂度 O(n²),元素基本有序时最好时间复杂度 O(n),空间复杂度 O(1)。
比较相邻的元素,如果第一个比第二个大就进行交换,对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,每一轮排序后末尾元素都是有序的,针对 n 个元素重复以上步骤 n -1 次排序完毕。
当序列已经有序时仍会进行不必要的比较,可以设置一个标志记录是否有元素交换,如果没有直接结束比较。
是对冒泡排序的一种改进,不稳定,平均/最好时间复杂度 O(nlogn),元素基本有序时最坏时间复杂度 O(n²),空间复杂度 O(logn)。
首先选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全部小于等于基准元素,一部分全部大于等于基准元素,再按此方法递归对这两部分数据进行快速排序。
快速排序的一次划分从两头交替搜索,直到 low 和 high 指针重合,一趟时间复杂度 O(n),整个算法的时间复杂度与划分趟数有关。
最好情况是每次划分选择的中间数恰好将当前序列等分,经过 log(n) 趟划分便可得到长度为 1 的子表,这样时间复杂度 O(nlogn)。
最坏情况是每次所选中间数是当前序列中的最大或最小元素,这使每次划分所得子表其中一个为空表 ,这样长度为 n 的数据表需要 n 趟划分,整个排序时间复杂度 O(n²)。
归并排序基于归并操作,是一种稳定的排序算法,任何情况时间复杂度都为 O(nlogn),空间复杂度为 O(n)。
基本原理:应用分治法将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并,使用一个辅助空间并设定两个指针分别指向两个有序序列的起始元素,将指针对应的较小元素添加到辅助空间,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。
适用场景:数据量大且对稳定性有要求的情况。
数据量规模较小,考虑直接插入或直接选择。当元素分布有序时直接插入将大大减少比较和移动记录的次数,如果不要求稳定性,可以使用直接选择,效率略高于直接插入。
数据量规模中等,选择希尔排序。
数据量规模较大,考虑堆排序(元素分布接近正序或逆序)、快速排序(元素分布随机)和归并排序(稳定性)。
TCP的Socket套接字是一种可靠的网络通信协议,它可以确保数据的完整性、有序性和可靠性。在C++中实现TCP的Socket套接字的客户端与服务端通信,主要分为以下几个步骤:
- 创建Socket:使用socket()函数创建一个Socket对象,指定协议类型和通信方式。
- 绑定地址:服务端需要使用bind()函数将Socket对象与一个本地地址绑定,客户端则不需要。
- 监听连接:服务端需要使用listen()函数开始监听客户端的连接请求,客户端不需要。
- 建立连接:客户端使用connect()函数向服务端发起连接请求,服务端使用accept()函数接受客户端的连接请求。
- 通信:客户端和服务端通过send()和recv()函数进行数据的发送和接收。
- 关闭连接:客户端和服务端使用close()函数关闭连接。
下面是一个简单的TCP Socket通信的例子,其中包含了客户端和服务端的代码:
服务端代码:
客户端代码:
在上面的例子中,服务端监听本地地址8080端口的连接请求,并在接受到客户端的连接请求后,接收客户端发送的消息,并发送一个回复消息。客户端连接到本地地址为127.0.0.1的8080端口,并发送一条消息给服务端,然后接收服务端发送的回复消息。最后,客户端和服务端都关闭连接。
UDP的Socket套接字是一种无连接的网络通信协议,它不保证数据的可靠性和有序性,但是具有较低的延迟和较高的传输速率。在C++中实现UDP的Socket套接字的客户端与服务端通信,主要分为以下几个步骤:
- 创建Socket:使用socket()函数创建一个Socket对象,指定协议类型和通信方式。
- 绑定地址:服务端和客户端都可以使用bind()函数将Socket对象与一个本地地址绑定。
- 通信:客户端和服务端通过sendto()和recvfrom()函数进行数据的发送和接收。
- 关闭Socket:客户端和服务端使用close()函数关闭Socket。
下面是一个简单的UDP Socket通信的例子,其中包含了客户端和服务端的代码:
服务端代码:
客户端代码:
在上面的例子中,服务端监听本地地址8080端口的UDP连接请求,并在接受到客户端的连接请求后,接收客户端发送的消息,并发送一个回复消息。客户端连接到本地地址为127.0.0.1的8080端口,并发送一条消息给服务端,然后接收服务端发送的回复消息。最后,客户端和服务端都关闭Socket。
- 连接数据库: mysql -u username -p password -h hostname
- 显示数据库列表: show databases;
- 创建数据库: create database dbname;
- 选择数据库: use dbname;
- 显示数据表: show tables;
- 创建数据表: create table tablename(column1 datatype, column2 datatype, …);
- 插入数据: insert into tablename(column1, column2, …) values(value1, value2, …);
- 查询数据: select * from tablename;
- 更新数据: update tablename set column1=value1, column2=value2 where condition;
- 删除数据: delete from tablename where condition;
- 删除数据表: drop table tablename;
- 删除数据库: drop database dbname;
- 导入数据: source filename.sql;
- 导出数据: mysqldump -u username -p password dbname > filename.sql
以下是一个具体的例子,假设我们有一个学生表(students)和一个成绩表(scores),它们的结构如下:
学生表(students):
成绩表(scores):
现在我们可以使用以下命令和操作来查询和处理这些数据:现在我们可以使用以下命令和操作来查询和处理这些数据:
- 连接数据库: mysql -u root -p password -h localhost
- 显示数据库列表: show databases;
- 创建数据库: create database mydb;
- 选择数据库: use mydb;
- 显示数据表: show tables;
- 创建数据表:
- 插入数据:
- 查询数据:
- 更新数据:
- 删除数据:
- 删除数据表:
- 删除数据库:
- 导入数据:
- 导出数据:
- Linux是一种开源的、免费的操作系统,其内核由Linus Torvalds在1991年创建,现在已经成为了世界上最流行的操作系统之一。
- 常用命令:
- ls:列出目录下的文件和子目录。
- cd:切换当前目录。
- pwd:显示当前所在目录的路径。
- mkdir:创建一个新目录。
- rm:删除文件或目录。
- cp:复制文件或目录。
- mv:移动或重命名文件或目录。
- cat:查看文件内容。
- grep:在文件中查找指定字符串。
- chmod:修改文件或目录的权限。
Vim是一款文本编辑器,常用于Linux和Unix系统中。它是Vi编辑器的改进版,具有强大的编辑功能和高度的可定制性。以下是Vim的一些常用命令:
- i:在当前光标位置插入文本。
- a:在当前光标位置的下一个字符处插入文本。
- o:在当前光标行的下一行插入新行。
- Esc:退出插入模式,回到命令模式。
- :w:保存文件。
- :q:退出Vim。
- :q!:强制退出Vim,不保存修改。
- :wq:保存文件并退出Vim。
- yy:复制当前行。
- p:将复制的文本粘贴到当前光标位置。
- dd:删除当前行。
除了这些基本命令之外,Vim还有许多其他的高级功能,如宏录制、多窗口编辑、代码折叠等等。Vim的学习曲线可能比较陡峭,但是一旦掌握了它的基本操作,就能够提高文本编辑的效率。
- 下面是一个简单的C++程序,使用Vim编辑器进行编辑:
- 打开一个终端窗口,输入以下命令启动Vim编辑器:vim hello.cpp这将创建一个名为“hello.cpp”的新文件,并在Vim中打开它。
- 进入插入模式,输入以下代码:#include <iostream>int main(){std::cout << \”Hello, world!\” << std::endl;return 0;}这个程序会输出“Hello, world!”,然后返回0。
- 保存文件并退出Vim。首先按下Esc键,回到命令模式,然后输入以下命令::wq这将保存文件并退出Vim。
- 使用GCC编译器编译程序。在终端窗口中输入以下命令:g++ -o hello hello.cpp这将生成一个名为“hello”的可执行文件。
- 运行程序。在终端窗口中输入以下命令:./hello这将运行程序,并输出“Hello, world!”。
Qt是一种跨平台的GUI应用程序开发框架,提供了一系列的工具和类库,使开发者可以方便地开发高质量的跨平台应用程序。Qt的主要内容包括以下几个方面:
- Qt核心模块:提供了Qt的基本功能,包括对象模型、信号和槽机制、容器类、文件和IO操作、多线程等。
- Qt Widgets模块:提供了一系列基于QWidget的GUI控件,如按钮、标签、文本框等,用于构建传统的桌面应用程序。
- Qt Quick模块:提供了一种基于QML的声明式语言,用于构建现代的用户界面,支持动画、视觉效果等。
- Qt网络模块:提供了网络编程相关的类库,如TCP和UDP套接字、HTTP和FTP协议等。
- Qt数据库模块:提供了用于访问各种关系型数据库的类库,如MySQL、PostgreSQL、SQLite等。
- Qt多媒体模块:提供了音频和视频处理相关的类库,如播放音频和视频、录制音频和视频等。
- Qt图形和绘图模块:提供了用于2D和3D图形渲染相关的类库,如OpenGL、QPainter等。
- Qt Web模块:提供了用于Web开发相关的类库,如Webkit和WebEngine。
除了以上这些模块之外,Qt还提供了一些其他的工具和类库,如Qt Creator集成开发环境、Qt Quick Controls用于构建移动应用程序等。总的来说,Qt是一个功能强大、可扩展、易于使用的开发框架,适用于开发各种不同类型的跨平台应用程序。
信号和槽机制是Qt框架中的一种事件处理机制,用于在对象之间进行通信。信号是一种特殊的函数,当特定的事件发生时,会自动调用信号,而槽则是一种函数,用于响应信号。
在Qt中,一个对象可以有多个信号和槽,它们可以在不同的对象之间连接起来,实现对象之间的通信。当信号被触发时,与之连接的槽函数会被自动调用,从而实现了对象之间的协作。
使用信号和槽机制可以使代码更加灵活、可维护和可扩展。它可以帮助开发人员更好地组织代码,降低代码的耦合度,并提高程序的可读性和可维护性。
下面是一个简单的例子,演示如何在两个对象之间使用信号和槽机制进行通信。
假设有一个窗口类和一个按钮类,当按钮被点击时,窗口会显示一个消息框。代码如下:
在上面的代码中,我们创建了一个窗口类MyWindow和一个按钮类MyButton。在窗口类的构造函数中,我们创建了一个按钮,并将其连接到窗口类的showMessage()槽函数上。当按钮被点击时,就会触发clicked()信号,进而自动调用showMessage()槽函数,从而显示一个消息框。
这就是一个简单的使用信号和槽机制的例子。通过信号和槽的连接,我们实现了按钮和窗口之间的通信,使程序更加灵活和可扩展。
以下是一个使用Qt5的版本的信号和槽机制的例子:
在这个例子中,我们使用了Qt5的新语法来连接信号和槽。具体来说,我们使用了connect()函数的新语法,将信号和槽通过函数指针连接起来。
在按钮类的构造函数中,我们创建了一个按钮,并将其连接到窗口类的showMessage()槽函数上。当按钮被点击时,就会触发clicked()信号,进而自动调用showMessage()槽函数,从而显示一个消息框。
这个例子和之前的例子类似,只是使用了Qt5的新语法来连接信号和槽。这种新语法更加简洁明了,使代码更加可读性和可维护性。
- QMainWindow:主窗口,包括菜单栏、工具栏、状态栏等。例如:Qt Creator的主窗口。
- QDialog:对话框窗口,用于显示对话框,例如:文件选择对话框、颜色选择对话框等。
- QWidget:基本窗口,用于显示各种控件,例如:按钮、文本框、标签等。
- QDockWidget:浮动窗口,可以被拖拽到主窗口的四周,例如:Qt Creator的工程浏览器、属性编辑器等。
- QSplashScreen:启动画面,用于显示程序启动时的欢迎画面。例如:各种桌面应用程序的启动画面。
Qt中常用的控件有很多,下面列举一些常用的控件,并给出简单的例子:
- QPushButton(按钮控件)
- QLabel(标签控件)
- QLineEdit(文本框控件)
- QTextEdit(多行文本框控件)
- QComboBox(下拉框控件)
- QSpinBox(数字框控件)
- QSlider(滑动条控件)
这些控件只是Qt中的一部分,Qt还提供了很多其他的控件,如QCheckBox、QRadioButton、QTableWidget、QTreeWidget等等。这些控件可以帮助我们快速构建各种类型的界面,提高开发效率。
Qt中的样式表是一种用于定制控件外观的机制,类似于CSS。通过样式表,可以改变控件的颜色、字体、边框等属性,从而实现自定义的外观效果。
以下是一些常用的样式表属性和例子:
- background-color:设置控件的背景颜色。例如:QPushButton { background-color: red; }
- color:设置控件的前景颜色(即文本颜色)。例如:QLabel { color: blue; }
- font-size:设置控件的字体大小。例如:QLineEdit { font-size: 12px; }
- border:设置控件的边框。例如:QGroupBox { border: 1px solid black; }
- padding:设置控件的内边距(即控件内容与边框之间的距离)。例如:QLineEdit { padding: 5px; }
- margin:设置控件的外边距(即控件边框与周围控件之间的距离)。例如:QLabel { margin: 10px; }
这些样式表属性可以组合使用,实现更丰富的外观效果。例如,以下样式表将QPushButton的背景颜色设置为蓝色,边框为圆角,字体为白色,内边距为10px:
QPushButton { background-color: blue; border-radius: 5px; color: white; padding: 10px; }
学习C++不是一蹴而就的,是终身的,基础部分十分的重要,需要反复的学习和体会,学习的过程中,出现了错误才深刻。
C++入门之实战(1)
最近更新的有点慢,主要是因为我自己有点纠结了,有点不知道应该写一些什么内容了。我想,除了介绍一些理论知识,还要穿插一些实战才可以,但是实战的内容不好写,我在考虑是否要录一些视频,视频在表达起来更容易,呈现的内容也更多。但我也深知文章对人的重要性,有时候文章接受起来更容易,特别是短小精简的文章。
因为是入门系列文章,今天介绍一个非常简单的例子。
一个最简单的C++工程项目,只需要一个源代码文件就可以实现,那就是main.cpp文件。在c++开发环境中,程序运行都是从main()函数开始的。然而在实践中,不可能只用一个mian.cpp文件写完所有代码。在一般的编程规范中,c++工程项目中一般有三种文件:头文件、源代码文件和main.cpp文件。
其中,main函数声明和定义在main.cpp文件中, 头文件用来一般类和函数等的声明,源代码文件用来一般类和函数等的定义。在之前的几篇文章中,我都将类的声明和定义写在了头文件中,也即在类的声明处定义该类,也不是不行,但不一定符合规范。声明和定义应该是要分开的,除非函数的实现很短小(只有几行)。
图1 基本的工程结构
如图1所示是c++基本的工程结构,在helloworld.h中是类HelloWorld的声明,在helloworld.cpp中的类的定义。在mian.cpp中是主函数main().(我用的是qtcreator,下载安装qt5以上自带qtcreator,有时间,我做一个专门说明IDE的文章或视频)。
图1 helloworld.h文件
图2 helloworld.cpp
在helloworld.h中HelloWorld是类名,以class关键字来声明。HelloWorld和~HelloWorld分别是构造函数和析构函数,函数名就是类名。void Show();是成员函数。string _str是成员变量又叫数据成员。可以看到,在helloworld.h中只进行了相关元素的声明。另外,在写头文件的时候一般在文件开头加入图上的宏定义,宏定义名称在不同的文件中是不同的,以避免一个头文件被#include 多次,在编译的时候报重定义的错误。
在helloworld.cpp中对类中的成员函数进行定义,所谓定义,也就是对函数们进行了实现,将函数中实现的逻辑写了出来。其中 :: 表示域作用符,HelloWorld::Show()表示Show是在类HelloWorld中声明的。再定义构造函数的时候,注意到_str的初始化没有,那个叫做通过初始化列表来初始化成员变量,这样进行初始化,比在{ }内初始化得更早。当然在helloworld.cpp中首先要#include \”helloworld.h\”.
在main.cpp中,使用HelloWorld类,首先#include \”helloworld.h\”.然后用法如图上所示。
另外,提示,要是用string,则要#include<string> ,要使用cout ,则要#include <iostrean>.
#include \”xxx\” 和 #include <mmm>的区别在于,xxx是自己写的头文件,mmm是系统头文件。
这是一个最简单的工程,体现了一点面向对象的思想,即将helloworld封装成一个类。这也是c++代码的基本组织方式。
今天就先到这里,今天码字比较多,有点违反我的“简单”原则,我想下一次类似实战的文章还是录视频能更说清楚。我的目的就是希望帮大家更容易入门,因为我知道自己当初有多痛苦。
谢谢阅览,希望多提宝贵意见,也顺便关注一下,以后大家多多交流,共同进步。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。