从C到CPP的迁移
引入
为什么会有这篇笔记
面向过程和面向对象
作用域
基本语法内的新特性
引用
在C语言中仅有值传递和地址传递,而C++除此之外还可以进行引用传递。
默认参数
C语言中若要使用默认参数,仅能通过宏实现。
函数重载
函数重载基本用法
函数重载是为了提高函数名复用性或是使得函数兼容更多数据类型诞生的。它允许两个函数使用同样的名称,通过传入的参数不同来进行区分。C++编程中支持函数重载,而C不支持。
所谓传入的参数不同,可以是参数的类型不同,例如func(int a)
和func(double a)
;也可以是参数的个数不同,例如func(int a)
和func(int a, int b)
;还可以是顺序不同,例如func(int a, double b)
和func(double b, int a)
。
例如如下示例。下面两个函数,虽说它们的名字相同,但是由于一个参数类型是int,一个参数类型是double。在调用func
函数时,若传入func(1)
,则调用第一个;若传入func(3.14)
,则调用第二个。
1 | void func(int a){ |
函数重载+默认参数
当函数中使用了默认参数时,需要注意在忽略默认参数后函数参数有差异。例如如下示例。
1 | void func(int a, int b=0); |
此时,在传入诸如func(233)
的时候,编译器无法区分是调用默认b=0的func
还是仅传入a的func
,因此会报错。
const修饰参数的重载
有时会通过在传入函数时用const
修饰来避免值在函数内被修改。例如string.h
库内的strcmp(const char* str1, const char* str2)
。普通char
类型也可以传入const char*
(部分编译器会给个警告,有些编译器连警告都没有),但是如果用const
区分了函数重载,优先使用最匹配的类型。例如如下代码:
1 | void func(char* a); |
那么,在char a = 'H';
后,调用func(a)
,会优先调用func(char* a)
。因为他们参数类型匹配。如果要调用func(const char* a)
,则需要进行强制类型转换func((const char*) a)
。
当然,在传入func('H')
时,也是调用第二个,因为'H'
在编译器中时默认申领了一个const
类型的变量再进行传递。
用new在堆区开辟内存
类和对象
类的封装
类和对象是什么
- 类由 成员变量 和 成员函数 构成
- 成员变量被称为该类的 属性
- 成员函数被称为该类的 方法
- 类中所囊括的东西(包含属性和行为)被称为类的成员。
类的权限
类的访问权限有三种:
- public:成员在 类内、类外均可访问。
- protected:成员在 类内可访问,类外不可访问。在继承时,子类可以访问父类该权限下的成员。
- private:成员在 类内可访问,类外不可访问。在继承时,子类无权访问父类该权限下的成员。
通过属性私有、方法公开的方式,实现C中的断言机制
熟悉STM32 HAL库的同学可能经常见到assert
断言宏,几乎在HAL库的每一个库函数内都会调用断言宏来判断输入的参数是否合理。在C++中,可以通过将属性放在private,操作该属性的函数放在public来实现这一点。
举个例子,现在有一个重庆邮电大学的类。该类内有一个属性是重庆邮电大学的title,将其定义为private,并在public内设getTitle()
和setTitle()
函数去操作它。在setTitle()
内写当发现这个title = 985或者title = 211的时候,都认为它是非法的,不予赋值。因为title处于私有,因此不可外部直接赋值,必须通过setTitle()
进行赋值,而setTitle()
又避免了211或者985这样的非法参数。此时就对输入参数的合法性做出了断言。
代码如下:
1 | class CQUPT |
类中的this指针
在上文的示例代码中可以看到,为了将外部传入的title和类内自己的title
区分开,外部传入的被定义为了i_title
。但是有没有什么更方便的解决方法呢?答案就是this指针。在类内使用this指针可以指向自己的成员变量。下面是修改后的代码。
1 | class CQUPT |
可以看到以上代码在gitTitle()
处是没有写成this->title
的,这是因为这里不存在命名冲突,编译器会自动给title
它理解为this->title
。
this指针其实是一个指向基于该类创建的对象的内存空间的指针常量,编译会自动给他一个定义是CQUPT* const this
。这个指针会在类的某个成员函数被创建时进行定义。例如上述代码中,setTitle中的this指针和getTitle的类指针其实是独立定义的,都定义在他们函数各自的开头。
this指针与常函数
在类中如果要避免成员函数修改成员变量的值,在函数名称后加一个const将其声明为常函数即可。在成员函数屁股后面加了const之后,this指针会被创建为const CQUPT* const this
,此时this指向的所有值都不可能更改了。例如下面代码
1 | class CQUPT |
如果创建的对象是一个常量,例如const CQUPT lese
,那么lese
对象中就只允许存在常函数,不允许普通函数存在。
对象下的链式编程
运算符重载
运算符重载是指对已有运算符进行定义,赋予其另一种功能,以适应不同数据类型。
想象一个场景:现在有一个对象,内含有int data1, int data2
数据。使用该结构体定义了两个变量a1,a2
,当我写出a1+a2
时,希望a1
的data1
加上a2
的data1
,a1
的data2加上
a2的
data2`。
如果在C语言中,需要定义一个宏运算或使用一个函数来完成这个操作。但是C++中的运算符重载允许对”+”进行重载,这样每次调用
运算符重载的关键字为operatorX
,其中X替换为需要被重载的运算符。例如operator+
。运算符重载可以在类中进行,也可以在全局函数初进行。
下面是在类中进行运算符重载的例子:
1 | class data |
类的6大基本构成
在C++中,就算创造一个空类,也会默认包含这六个成员——构造函数、拷贝构造函数、析构函数、赋值操作重载、取地址操作符重载、const修饰的取地址操作符重载。
引入:对象的初始化和清理——(拷贝)构造函数和析构函数
C++的对象同变量是一样的。如果在某个函数内创建则创建在栈区;如果在函数外创建则创建在全局区(在部分单片机上认为全局区就是堆区);如果使用new创建则创建在堆区。
若是栈区对象,同变量一样,当调用某个函数时,这个对象会在栈区被创建,当函数return在回收这一帧栈帧时,对象也会被回收。
在创建对象时,会自动调用构造函数将其成员初始化。在这个对象被释放时,会自动调用析构函数对其进行清理。构造函数和析构函数可以人为定义,如果人为定义留空,则编译器自动补充一个空函数(对于拷贝构造函数而言是全属性赋值函数,下面会详细介绍)。
1. 初始化类属性——构造函数
直接使用构造函数内进行赋值,来初始化属性:
构造函数是与类同名的函数,函数无返回值,定义也无需加返回类型。例如下例:
1 | class example{ |
在使用example obj;
创建对象时,就会自动调用example()
函数。构造
构造函数是支持传入参数的,可以在创建对象时利用括号传入,例如下面代码就传入了10:
1 | class example{ |
有部分教程会给出其他的赋值方法,利用显式、隐式转换等等,那些方法在部分编译器上是可用的(例如VC++)。但是部分编译器不!支!持!
例如DevCpp IDE就会对 example test = example(10)
和 example test = 10
这种写法报错,仅允许example test = test0
(test0是另一个同类对象)。更何况,在嵌入式系统下编译器都是经过魔改的(例如Arduino)因此强烈建议不要使用。
举一个实际应用的例子:
1 | class student{ |
使用初始化列表来初始化属性:
除了像上面那么写之外,还可以使用初始化列表的语法来写构造函数。语法是:构造函数(传入参数):属性(值),属性(值),属性(值)...{其他语句}
。值得注意的是,初始化列表无需使用this指针。
举个例子:
1 | class student{ |
2. 特殊的构造函数——拷贝构造函数
同时,构造函数也支持函数重载,即可以定义多个构造函数,根据创建对象时传入的参数来判定调用哪个进行初始化。那么定义多个构造函数有啥用呢?一方面它可以允许不同的初始化方式;另一方面构造函数可以被看成普通构造函数和拷贝构造函数两个大类,拷贝构造函数将在该对象值被传递时调用。下面将详细演示
普通构造函数就是直接传入属性初始化参数或甚至不传入参数,来对属性进行赋值和初始化的。而拷贝构造函数的参数是这个对象本身,其是传入一个同类型的对象,然后将这个对象内的属性“拷贝”到这个新创建的对象上去。下面的代码就展示了一个普通构造函数和一个拷贝构造函数。
1 | class student{ |
不难发现,其实所谓拷贝构造函数和普通构造函数并无差异,无非就一个特殊一点的,以传入一个同类对象为参数的函数罢了。那拷贝构造函数有什么意义呢?在下面这些场景中,就可以用拷贝构造函数进行初始化:
- 已经构建出一个对象,需要创建这个对象当前状态的一个副本
- 对象在函数间被传递的时候
第一个很好理解,有些时候需要创建xxx对象的副本xxx_old,此时如果用普通构造函数传参进去来初始化,则需要xxx.属性1
,xxx.属性2
这样一个一个地访问。而拷贝构造函数可以直接传入xxx对象,在函数内逐个访问进行赋值。例如下面这段代码,就创建了一个stu_old
做为stu1
的副本。
1 | class student{ |
对象在函数间被传递调用拷贝构造函数是指的如下这种情况,在下面的代码中,函数func
接受的参数是stu对象,在调用时传入了stu1
;此时这个函数内的stu对象就会调用拷贝构造函数拷贝stu1
对它进行初始化。在return时,使用stu2
对象接住了函数return的stu
,此时stu2
也是调用拷贝构造函数将stu
内的状态拷贝至stu2
内。
1 | class student{ |
- 当用户未定义普通构造函数或拷贝构造函数时,编译器默认提供一个空函数;
- 当用户未定义拷贝构造函数时,编译器默认提供一个全属性拷贝的拷贝构造函数;
- 一旦用户提供了拷贝构造函数,编译器也不会提供普通构造函数
3. 清理类属性——析构函数
前面提到,析构函数是在编译器在释放对象内存时调用的。回想一下,在C系语言中,在栈区开辟的内存由操作系统回收,在堆区开辟的内存由程序员自行管理。对象内可能含有即存在于栈区的变量,又含有存在于堆区的变量,在释放对象时,堆区变量就需要程序员自行清理了。在析构函数中写入释放堆区变量,就可以实现对象被释放时手动管理堆区变量了。
在类中定义一个与类同名,但前面加~
符号的函数,就是析构函数。例如类A,析构函数就是~A()
。析构函数不具有输入和输出,也无需声明return数据类型。
例如如下代码
1 | class student{ |
4.赋值操作重载
5.取地址操作符重载
6.const修饰的取地址操作符重载。
堆区内存的深拷贝和浅拷贝
在前面提到可以用析构函数来释放堆区内存,但是这存在一个问题:在对该对象进行拷贝之后,可能会导致内存重复释放。参考下面示例
1 | class student{ |
运行会发现这个程序会崩溃,这是为啥?原因就出在int* age
这个指针。new在堆区开辟内存空间之后,将其地址赋给了指针age。在student stu2 = stu1
进行拷贝时,拷贝的是age指针中的地址值,即,stu2和stu1中的age指针指向同一个堆区地址。
这就带来了free的问题。在return时,首先delete了stu1的age指向的地址。然后又想delete stu2的age指向的地址,然而这两个指向的却是同一个已被删除的地址,因此程序崩溃。拷贝对象时,将指针地址值拷贝走,而没有开辟新的内存的问题,就被称为浅拷贝
要解决这个问题,就需要使用深拷贝,深拷贝旨在自定义拷贝构造函数,在拷贝构造函数内遇到堆区变量的拷贝,使用new
创建新的空间后,再传递原堆区变量值。如下是深拷贝的代码
1 | class student{ |
前面介绍一个类里面默认含一个赋值操作符重载函数。只需要在重载赋值运算符的函数内和拷贝构造函数内,写入深拷贝的函数,就可以避免采用 = 赋值操作时产生浅拷贝问题。
类的内存布局
类中成员被static修饰
static
关键字在C中也有所使用,其会申领存在于全局区内的静态变量,而且只初始化一次,在不改变作用域的前提下(例如在某个函数内定义的,这个变量依旧只能在这个函数内调用),有较长的生命周期(函数return的时候该变量并不会被释放,而是一直存在)。
在C++中,如果类内存在被static修饰的成员变量或成员函数,则其也是被存放在全局区的,而且也不会重复初始化。无论这个类创建了多少个对象,所有对象都共享同一个内存。同时,静态变量也可以使用类名进行访问,例如student
类内有静态age
变量,则可以通过student::age
进行访问。
在C++中,需要在类内声明静态变量,在全局变量的位置再次声明和初始化(如果在诸如main函数等函数内再次声明和初始化,是会报错的),例如如下代码。
1 | class student{ |
如果用static修饰成员函数,则该成员函数只能访问静态成员变量,因为不同对象调用的函数内存都是同一份,无法区分是要访问哪个对象内的成员。
类中成员被const修饰
类中的成员变量同类外面一样,const修饰不影响其地址。例如一个变量在全局变量的位置,且被const修饰,那它就被放在全局变量的位置。如果一个变量在函数中声明,且被const修饰,那它就是被放在栈中的,且函数return自动回收。
在类里面也是一样的,const声明的变量会存在于由这个类创建的实例对象的内存内,不同的示例访问的都是各自不同的内存。不存在static修饰后那样的共享情况。
类中内存布局
现在我们知道,类中会存在成员变量和成员函数。前面又介绍如果成员变量被static
修饰,则会放在全局变量区。对于类内的函数,其也是放在代码区的。类自己的内存区(也就是实例,类创建出的对象占用的内存空间)只会存放非静态成员函数和虚函数指针。
归纳一下就是:
- 类的成员函数被放在代码区。
- 类的静态成员变量在全局数据区。
- 非静态成员变量在类的实例(对象)内,随实例创建在栈还是堆决定。
- 虚函数指针、虚基类指针在类的实例内,随实例创建在栈还是堆决定。
友元
在类的封装中,介绍了类有三种不同的权限。私有权限是仅有自己能访问的,保护最好的。但是私密的东西也不一定只能自己看,就比如说地球OL中存在一个名为集美的特殊类,它有别于常见生物的类型,它的private生怕含money属性的对象不能访问,又生怕不含money属性的对象访问了。此时我们就需要用到友元了。
友元旨在给予一个函数或类权限,使其可以访问自己的private内的函数或变量。其中这个函数可以是某个类中的成员函数。
- 若将函数设置为友元,则可以在函数内访问和操作该类的私有成员。
- 若将类设置为友元,则可以在友元类的所有成员函数内访问和操作该类的私有成员。
- 若将类中的某一个函数设置为友元,则使用该类规则新建的对象中的这个函数可以该类的私有成员。
友元的关键字为firend
,要将某个东西设置为友元,只需要在类的最前面使用friend <函数/类声明>
进行声明就可以了。举个例子:
1 | class jiMei |
在上面这个例子中,因为爆金币函数在jiMei类中被设置为了friend,因此可以访问jiMei类中的私有成员,也就是可以访问根据jiMei类创建的对象a中私有的nanGuiMi变量了。
- 函数的友元声明方式为:
friend <函数声明>
,例如friend void spurtMoney();
- 类的友元声明方式为:
friend <类声明>
,例如friend class a;
- 类中某个函数的友元声明方式为:
firend <函数返回类型 类::函数>
,例如将类a中的void test()
函数声明为友元,就是friend void a::test();
继承
多态
STL库
(Standard Template Library,STL,C++标准模板库)