目录
一、C++简介
C++的发展史
1983年,贝尔实验室(Bell Labs)的Bjarne Stroustrup发明了C++。 C++在C语言的基础上进行了扩充和完善,是一种面向对象程序设计(OOP)语言。
C++的源文件扩展名是cpp
Stroustrup说:“这个名字象征着源自于C语言变化的自然演进”。还处于发展完善阶段时被称为“new C”,之后被称为“C with Class”。C++被视为C语言的上层结构,1983年Rick Mascitti建议使用C++这个名字,就是源于C语言中的“++”操作符(变量自增)。而且在共同的命名约定中,使用“+”以表示增强的程序。
常用于系统开发,引擎开发、嵌入式开发等应用领域, 至今仍然是最受广大程序员喜爱的编程语言之一。
C++在嵌入式中可以用于:
系统开发、算法开发、图形用户界面(GUI)开发。
C++特点
- 在支持C语言的基础上,全面支持面向对象开发。
- 编程领域广泛,功能强大(最难的编程语言之一)。
- 标准保持更新,本次课程以ISO C++98标准为主,以ISO C++11标准为辅。
- 为数不多的支持底层操作的面向对象语言。
- 在面向对象的编程语言中执行效率极高。
重要技术点
- 类 class
- 对象 object、instance......
- 面向对象的三大特性:封装 → 继承 → 多态
面向过程和面向对象
面向过程的编程思想:
1. (我)把大象剁成块
2. (我)打开冰箱门
3. (我)把大象装进冰箱
4. (我)关上冰箱门
面向过程的语言,关注的重点是“算法”,可以认为是一系列有序的步骤,只要按照这个步骤来做,就能得到预期的结果,这种思考方式更接近于计算机的思考方式。优点是可以把控到每个细节,因此程序的执行效率高;缺点是开发效率低。
为了提升软件项目的开发效率,发明了面向对象的编程思想。相比于面向过程,更注重上层对代码管理。
面向对象的编程思想:
1. (我)把大象和冰箱拟人化(实例化)
2. (我)给大象和冰箱下达任务
3. 大象和冰箱自己完成任务
面向对象的编程语言,关注重点是“对象”,可以认为“对象”的本质是一系列由于某种联系聚焦在一起数据单元体。不同单元体之间进行数据交互,从而实现代码运行的结果,程序员只需要站在更高层次去管理这些单元体,这种思考方式更接近人类的思考方式。优点是程序的开发效率高;缺点是执行效率低。
第一个C++程序
// 引入C++的标准IO头文件,C++标准头文件不写.h
// 如果要引入C头文件,需要编写.h
#include <iostream>
// 使用标准名字空间,后面讲,不要删
using namespace std;
/**
* @brief main 主函数程序的入口
* @return 随意
*/
int main()
{
// 输出一个字符串,endl是换行
cout << "Hello World!" << endl;
cout << "你好,世界!" << endl;
return 0;
}
二、从C到C++
1.引用 reference
1.1 概念
引用与指针类似,但是使用更加简便,功能更加简单,可以认为引用是一个变量的“别名”,对引用进行操作与直接操作变量完全相同。
#include <iostream>
using namespace std;
int main()
{
int a = 100;
int& b = a; // b是a的引用
b++;
cout << a << " " << b << endl; // 101 101
cout << &a << " " << &b << endl; // 0x61fe88 0x61fe88
return 0;
}
1.2 性质
1. 可以改变引用的变量值,但是不能再次成为其它变量的引用。
#include <iostream>
using namespace std;
int main()
{
int a = 100;
int& b = a; // b是a的引用
int c = 200;
b = c; // 赋值操作
cout << &a << " " << &b << " " << &c << endl; // 0x61fe88 0x61fe88 0x61fe84
// &b = c; 错误
// int& b = c; 错误
return 0;
}
2. 声明引用时,必须对其进行初始化。
#include <iostream>
using namespace std;
int main()
{
int a = 100;
// int& b; 错误
// b = a;
// cout << b << endl;
return 0;
}
3. 声明引用时初始化的值不能是NULL
#include <iostream>
using namespace std;
int main()
{
int a = 100;
// int& b = NULL; 错误
cout << b << endl;
return 0;
}
4. 声明引用时,初始化的值如果是纯数值,需要给引用增加const关键字修饰,表示该引用是常引用,这样的引用不能改变其数值。
#include <iostream>
using namespace std;
int main()
{
// int& b = 100; 错误
const int& b = 100;
cout << b << endl;
return 0;
}
5. 可以将变量引用的地址赋值给一个指针,此时指针指向的还是原来的变量。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int* c = &b; // 指针c指向b,相当于直接指向a
cout << *c << endl; // 10
return 0;
}
6. 可以对指针建立引用。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* p = &a; // 指针p指向a
int*& t = p; // t是p的引用
cout << t << " " << *t << endl; // 0x61fe88 10
return 0;
}
7. 可以使用const对引用进行修饰,此时虽然不能直接改变引用的值,但是可以间接改变原变量值。
#include <iostream>
using namespace std;
int main()
{
int a = 1;
const int& b = a; // b是a的常引用
// b++; 错误
a++;
cout << a << " " << b << endl; // 2 2
return 0;
}
1.3 引用参数
【思考】写一个函数,通过传入两个参数可以交换传入参数的两个整型数值。
#include <iostream>
using namespace std;
// 【思考】写一个函数,通过传入两个参数可以交换传入参数的两个整型数值。
/**
* @brief swap1 交换失败
*/
void swap1(int a,int b)
{
a = a^b;
b = a^b;
a = a^b;
cout << a << " " << b << endl;
}
/**
* @brief swap2 交换成功,但是繁琐
*/
void swap2(int* a,int* b)
{
*a = *a^*b;
*b = *a^*b;
*a = *a^*b;
}
/**
* @brief swap3 交换成功
*/
void swap3(int& a,int& b)
{
a = a^b;
b = a^b;
a = a^b;
}
int main()
{
int a = 1;
int b = 2;
swap1(a,b);
cout << a << " " << b << endl; // 1 2
swap2(&a,&b);
cout << a << " " << b << endl; // 2 1
swap3(a,b);
cout << a << " " << b << endl; // 1 2
return 0;
}
使用引用参数还可以使参数传递的效率提高,因为不产生副本。
引用参数应该在能被定义为const的情况下,尽量定义const,以达到引用的安全性。
void show(const int& a)
{
cout << a << endl;
}
2. 赋值
C++中除了可以使用=赋值外,还可以使用下面的方式赋值。
#include <iostream>
using namespace std;
int main()
{
int a(1); // 给a赋值1
int b(a); // 给b赋值a
cout << a << " " << b << endl; // 1 1
return 0;
}
C++11中增加了数据窄化的写法与提示。
#include <iostream>
using namespace std;
int main()
{
double a = 3.14;
int b(a); // 传统C++写法
int c{a}; // C++11写法:给出警告
cout << b << endl;
cout << c << endl;
return 0;
}
3. 键盘输入
在C++中可以使用cin来获得键盘输入,也支持连续输入,也在头文件iostream里。连续输入的内容可以使用空格或回车来分割。
cin输入的字符串类型是string,这是C++的字符串类型。
#include <iostream>
using namespace std;
int main()
{
string s; // 创建一个内容为空的字符串变量
cout << s << endl; // 无输出结果
int i;
cout << "请输入一个字符串和一个数字:" << endl;
cin >> s >> i;
cout << "请输入的内容是:" << s << endl << i << endl;
return 0;
}
4. 字符串类型 string
4.1 基础使用
string并不是C++的基本数据类型,string是C++内置的字符串类,使用时需要引入头文件<string>,而不是<string.h>,string在绝大多数情况下可以代替char*,不必担心内存是否足够和字符串长度。
string使用ASCII编码,只支持英文字符,严禁使用中文字符!!!
string内部集成了大量的字符串处理函数。
#include <iostream>
using namespace std;
int main()
{
string s = "yui%^";
// 获取字符串长度
cout << s.size() << " " << s.length() << endl; // 5 5
cout << "--------for循环遍历--------" << endl;
for(int i = 0;i<s.size();i++)
{
// 使用下标取出对应的字符
cout << s[i] << " ";
}
cout << endl;
// C++11支持
cout << "--------for each循环遍历--------" << endl;
for(char c:s)
{
cout << c << " ";
}
// 迭代器:略,后面讲解
return 0;
}
4.2 取出元素
string不光支持中括号 [ ] 的方式取出元素,还支持使用at函数的方式取出元素。
这两种方式的区别是:
- 前者取出的效率更高
- 后者更加安全
#include <iostream>
using namespace std;
int main()
{
string s = "yui%^";
// 使用中括号取出元素
cout << s[1] << endl; // u
// 使用at函数取出元素
cout << s.at(1) << endl; // u
// 范围越界
cout << s[-999] << endl; // '\0'
cout << s.at(20) << endl; // 运行终止
cout << "主函数执行完毕" << endl;
return 0;
}
5. 函数
5.1 内联函数 inline
内联函数的目的是取代宏定义的函数,使用关键字inline放在函数定义(非声明)的前面,可以将函数制定为内联函数。
内联函数在编译时,会直接展开函数体到主函数中编译,因此可以提升程序的运行效率。一般将代码长度较小(5行以内,不能包含复杂的控制语句)且频繁使用的函数写为内联函数。
#include <iostream>
using namespace std;
// 声明函数
void test();
// 定义函数
inline void test()
{
cout << "aaa" << endl;
}
int main()
{
test();
return 0;
}
所有的成员函数默认为内联函数,无需手动使用inline修饰。
5.2 函数重载 overload
C++中允许使用同一个函数名臣定义多个函数,这就是函数重载。函数重载的前提是各个重载的函数之间参数(类型或个数)不同,与返回值类型无关。
#include <iostream>
using namespace std;
void show()
{
cout << "1" << endl;
}
void show(int a)
{
cout << "2" << a << endl;
}
void show(double d)
{
cout << "2.5" << d << endl;
}
void show(string s)
{
cout << "3" << s << endl;
}
void show(string s1,string s2)
{
cout << "4" << s1 << s2 << endl;
}
int main()
{
show(1); // 21
show(3.14); // 2.53.14
show("Hello"); // 3Hello
show(); // 1
show("AA","BB"); // 4AABB
return 0;
}
除了上述的普通函数支持重载外,成员函数和构造函数等也支持函数重载,但是析构函数不支持函数重载。
5.3 函数的参数默认值(缺省值)
C++中允许给函数的参数设定默认值,在调用函数时,如果传入参数,则传入的参数会覆盖默认值;如果不传入参数,则使用默认值作为参数值。
参数的默认值只允许在声明和定义中出现一次。
#include <iostream>
using namespace std;
void func1(int a = 0); // 声明时制定参数默认值
void func2(int a);
void func1(int a)
{
cout << a << endl;
}
void func2(int a = 1) // 定义时制定参数默认值
{
cout << a << endl;
}
int main()
{
func1(); // 0
func1(666); // 666
func2(); // 1
func2(888); // 888
return 0;
}
向右(后)原则:如果函数参数有多个,此时给某个参数设定了默认值后,其右边(后边)所有的参数都必须设定默认值。
#include <iostream>
using namespace std;
void method1(int a,int b=1,int c=2)
{
cout << a << b << c << endl;
}
int main()
{
// method1(); 错误
method1(0); // 012
method1(0,0); // 002
return 0;
}
当函数的参数默认值与函数重载同时使用时,非常容易导致二义性问题(ambiguous),二义性问题指的是编译器无法做出两种情况的抉择。
尽量不要同时使用函数重载和默认值。
#include <iostream>
using namespace std;
void method(int a=0,int b=1,int c=2)
{
cout << a << b << c << endl;
}
void method()
{
cout << "aaa" << endl;
}
int main()
{
// method(); 错误
return 0;
}
5.4 哑元函数
如果一个函数的参数只有类型,没有名字,这个参数在函数体中无法使用,这样的参数被称为哑元,这样的函数被称为哑元函数。
#include <iostream>
using namespace std;
/**
* @brief test 哑元函数
*/
void test(string)
{
cout << "hahah" << endl;
}
int main()
{
test("Tom"); // hahah
return 0;
}
哑元函数的主要有以下用途:
- 区分重载函数,后面在运算符重载中使用。
- 保持函数的向前兼容性
三、面向对象基础
1. 类与对象的概念
类:类是对同一类对象的抽象总结,是一个概念。
对象:按照类的规定创建的实体。
程序员就是面向对象编程世界中的“上帝”,因此需要先写类的代码,才能按照这段代码创建对应的对象。参考类来创建对象的过程被称为“实例化”。因此,对象无法脱离对应的类存在。
一个类主要包括:
- 属性
用来描述对象的数据元素,通常是一个名词变量,例如:身高、体重、价格等,也称为“成员变量”或“数据成员”。
- 行为
用来描述对象执行的具体操作,通常对属性进行操作,以动词函数的方式存在,例如:吃饭、睡觉、运行等,也称为“成员函数”或“成员方法”。
成员变量和成员函数统称为“成员”。
2. 类的定义
【例子】以手机为例,来说明类的定义。
规定手机的属性:品牌、型号和重量
手机的成员函数:运行游戏、播放音乐、通信
#include <iostream>
using namespace std;
class MobilePhone // 帕斯卡命名法:所有单词的首字母大写
{
public: // 公有权限:最开放的一种权限
string brand; // 品牌
string model; // 型号
int weight; // 重量
void run_game() // 运行游戏
{
cout << "timi" << endl;
}
void play_music()
{
cout << "只因你太美" << endl;
}
void communicate()
{
cout << "你好" << endl;
}
};
int main()
{
return 0;
}
3. 实例化对象
类是一个抽象的概念,因此需要按照这个概念创建对应的对象实体,C++中有两种类型的对象:
- 栈内存对象
在生命周期结束(所在的花括号执行完)后,自动被销毁。
栈内存对象使用 . 调用成员。
- 堆内存对象
需要使用new关键字创建,使用delete关键字销毁,如果不销毁,则会持续存在,容易导致内存泄漏的问题,内存泄漏最终可能会导致程序卡顿,甚至卡死。
堆内存对象通常使用指针来保存堆内存对象的地址。
堆内存对象使用 -> 调用成员,在Qt Creator下,直接打.会转换成 ->
#include <iostream>
using namespace std;
class MobilePhone // 帕斯卡命名法:所有单词的首字母大写
{
public: // 公有权限:最开放的一种权限
string brand; // 品牌
string model; // 型号
int weight; // 重量
void run_game() // 运行游戏
{
cout << "timi" << endl;
}
void play_music()
{
cout << "只因你太美" << endl;
}
void communicate()
{
cout << "你好" << endl;
}
};
int main()
{
// 栈内存对象
MobilePhone mp1;
// 调用属性
mp1.brand = "小米";
mp1.model = "13 Pro";
mp1.weight = 216;
cout << mp1.brand << " " << mp1.model << " "
<< mp1.weight << endl;
// 调用成员函数
mp1.communicate();
mp1.play_music();
mp1.run_game();
// 堆内存对象
MobilePhone* mp2 = new MobilePhone;
// 调用属性
mp2->brand = "红米";
mp2->model = "K60 Pro";
mp2->weight = 206;
cout << mp2->brand << " " << mp2->model << " " <<
mp2->weight << endl;
// 调用成员函数
mp2->communicate();
mp2->play_music();
mp2->run_game();
// 手动销毁mp2
delete mp2;
// 有时销毁后还能使用部分功能,但是不要这么做
mp2 = NULL; // 可以赋值为NULL防止delete后调用
return 0;
}
// 主函数执行完,mp1销毁
4. 封装
软件测试有黑盒和白盒的概念,之前写的MobilePhone类是一个完全开放的类,所有类的细节都公开,类似于下图白盒。
封装作为面向对象的三大特性之一,要求将类的一些属性和细节隐藏,并重新公开给外部调用的接口,类似于下图黑盒。
封装的写法并不唯一,在实际的过程中会结合业务需求而定,下面的以一个最基础的案例进行编写。通常先对属性私有化,使属性隐藏,然后根据当前属性的需求,通过getter函数和setter函数对类外分别公开读和写的功能。
#include <iostream>
using namespace std;
class MobilePhone
{
private: // 私有权限:只有类内部可访问
string brand; // 可读可写
string model; // 只写
int weight = 188; // 初始值
public: // 公开接口
string get_brand() // getter:读属性
{
return brand;
}
void set_brand(string b) // setter:写属性
{
brand = b;
}
void set_model(string m) // setter
{
model = m;
}
int get_weight() // getter
{
return weight;
}
};
int main()
{
MobilePhone mp1;
mp1.set_brand("小米");
mp1.set_model("13 Pro");
cout << mp1.get_brand() << " " << mp1.get_weight() << endl;
MobilePhone* mp2 = new MobilePhone;
mp2->set_brand("红米");
mp2->set_model("K60 Pro");
cout << mp2->get_brand() << " " << mp2->get_weight() << endl;
delete mp2;
return 0;
}
封装有利于提升程序的安全性。
5. 构造函数
5.1 概念
构造函数类内一种特殊的函数,用来创建一个对象。如果一个类中,程序员不手动编写构造函数,编译器会为这个类自动添加一个无参构造函数,且此函数的函数体为空;如果程序员手写了任意一个构造函数,编译器就不再自动添加构造函数了。
构造函数要求函数名必须与类名完全一致,且构造函数无需写返回值。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
// MobilePhone(){} // 编译器自动添加的无参构造函数
MobilePhone() // 手写构造函数
{
// 可以设定属性的默认值
brand = "山寨";
model = "8848钛金手机";
weight = 188;
cout << "生产了一部手机" << endl;
}
string get_brand()
{
return brand;
}
void set_brand(string b)
{
brand = b;
}
void set_model(string m)
{
model = m;
}
int get_weight()
{
return weight;
}
};
int main()
{
MobilePhone mp1; // 调用了无参构造函数
mp1.set_brand("小米");
mp1.set_model("13 Pro");
cout << mp1.get_brand() << " " << mp1.get_weight() << endl;
MobilePhone* mp2 = new MobilePhone; // 调用了无参构造函数
mp2->set_brand("红米");
mp2->set_model("K60 Pro");
cout << mp2->get_brand() << " " << mp2->get_weight() << endl;
delete mp2;
return 0;
}
5.2 传参
可以给构造函数增加参数,使用参数给属性赋予初始值,使对象的创建更灵活。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
MobilePhone(string b,string m,int w)
{
brand = b;
model = m;
weight = w;
}
string get_brand()
{
return brand;
}
void set_brand(string b)
{
brand = b;
}
void set_model(string m)
{
model = m;
}
int get_weight()
{
return weight;
}
};
int main()
{
MobilePhone mp1("小米","13Pro",190); // 调用了三参构造函数
cout << mp1.get_brand() << " " << mp1.get_weight() << endl;
MobilePhone* mp2 = new MobilePhone("红米","K60Pro",199); // 调用了三参构造函数
cout << mp2->get_brand() << " " << mp2->get_weight() << endl;
delete mp2;
return 0;
}
5.3 重载
构造函数也支持函数重载,遵守之前函数重载的规则。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
MobilePhone() // 重载
{
brand = "8848";
model = "钛金手机";
weight = 199;
}
MobilePhone(string b,string m,int w) // 重载
{
brand = b;
model = m;
weight = w;
}
string get_brand()
{
return brand;
}
void set_brand(string b)
{
brand = b;
}
void set_model(string m)
{
model = m;
}
int get_weight()
{
return weight;
}
};
int main()
{
MobilePhone mp1("小米","13Pro",190); // 调用了三参构造函数
MobilePhone* mp2 = new MobilePhone("红米","K60Pro",199); // 调用了三参构造函数
delete mp2;
MobilePhone mp3; // 调用无参构造
MobilePhone* mp4 = new MobilePhone; // 调用无参构造
delete mp4;
return 0;
}
5.4 参数默认值
构造函数也支持之前的参数默认值设定。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
MobilePhone(string b = "8848",
string m="钛金手机",
int w = 199)
{
brand = b;
model = m;
weight = w;
}
string get_brand()
{
return brand;
}
void set_brand(string b)
{
brand = b;
}
void set_model(string m)
{
model = m;
}
int get_weight()
{
return weight;
}
};
int main()
{
MobilePhone mp1("小米","13Pro",190); // 调用了三参构造函数
MobilePhone* mp2 = new MobilePhone("红米","K60Pro",199); // 调用了三参构造函数
delete mp2;
MobilePhone mp3; // 调用无参构造
MobilePhone* mp4 = new MobilePhone; // 调用无参构造
delete mp4;
return 0;
}
5.5 构造初始化列表
构造初始化列表是一种简便的写法,可以用于给属性赋予初始值。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
MobilePhone(string b = "8848",
string m="钛金手机",
int w = 199):brand(b),model(m),weight(w)
{}
void show() // 展示所有属性值
{
cout << brand << " " << model << " "
<< weight << endl;
}
};
int main()
{
MobilePhone mp1("小米","13Pro",190);
mp1.show();
return 0;
}
在现有阶段,构造初始化列表可以根据实际需求自行决定是否采用。
5.6 拷贝构造函数
5.6.1 概念
如果程序员在一个类中不手动编写拷贝构造函数,编译器会为这个类自动添加一个拷贝构造函数,拷贝构造函数与普通构造函数也是函数重载的关系。
对象和对象之间是独立存在的实体,数据也是相互独立的,拷贝构造函数可以把一个对象的属性值拷贝到新创建的对象中。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
MobilePhone(string b,string m,int w)
:brand(b),model(m),weight(w)
{}
// 编译器自动添加的拷贝构造函数
// MobilePhone(const MobilePhone& mp)
// {
// brand = mp.brand;
// model = mp.model;
// weight = mp.weight;
// }
void show()
{
cout << brand << " " << model << " "
<< weight << endl;
cout << &brand << " " << &model << " "
<< &weight << endl;
}
};
int main()
{
MobilePhone mp1("小米","13Pro",190);
mp1.show();
// 拷贝构造函数
MobilePhone mp2(mp1);
mp2.show();
return 0;
}
【思考】拷贝构造函数可能会出现什么问题?
如果成员变量出现指针,在拷贝的过程中,会导致两个对象的成员变量指针保存同一份地址,指向同一个内存区域,不符合面向对象的特性。
5.6.2 浅拷贝与深拷贝
当类中的成员变量出现指针,默认的浅拷贝,代码如下:
#include <iostream>
#include <string.h>
using namespace std;
class Dog
{
private:
char* name; // 特例
public:
Dog(char* n)
{
name = n;
}
void show_name()
{
cout << name << endl;
}
};
int main()
{
char c[20] = "wangcai";
Dog d1(c); // d1.name → c
Dog d2(d1); // d2.name → c
strcpy(c,"tiedan");
d1.show_name(); // tiedan
d2.show_name(); // tiedan
return 0;
}
此时需要手写构造函数,为每个对象的name属性单独开辟一个内存区域,且当前对象独享这个区域。
#include <iostream>
#include <string.h>
using namespace std;
class Dog
{
private:
char* name; // 特例
public:
Dog(char* n)
{
name = new char[20];
// 只拷贝内容
strcpy(name,n);
}
Dog(const Dog& d)
{
name = new char[20];
// 只拷贝内容
strcpy(name,d.name);
}
void show_name()
{
cout << name << endl;
}
};
int main()
{
char c[20] = "wangcai";
Dog d1(c); // d1.name → 堆区1
Dog d2(d1); // d2.name → 堆区2
strcpy(c,"tiedan");
d1.show_name(); // wangcai
d2.show_name(); // wangcai
return 0;
}
其实在实际开发中,也可以采用屏蔽拷贝构造函数这种简单粗暴的方式解决浅拷贝问题。屏蔽某个构造函数只需要把这个构造函数的代码从public区域移动到private区域。
6. 析构函数
析构函数是与构造函数对立的函数。
#include <iostream>
using namespace std;
class Cat
{
public:
~Cat()
{
cout << "猫挂了" << endl;
}
};
int main()
{
cout << "主函数开始" << endl;
// Cat c1;
Cat* c2 = new Cat;
delete c2;
cout << "主函数结束" << endl;
return 0;
}
5.6.2节的深拷贝代码中,两个构造函数中都new出了name属性对应的堆内存空间,但是并没有回收,因此需要给Dog类增加析构函数,在析构函数中回收name占用的堆内存空间。
#include <iostream>
#include <string.h>
using namespace std;
class Dog
{
private:
char* name; // 特例
public:
Dog(char* n)
{
name = new char[20];
// 只拷贝内容
strcpy(name,n);
}
Dog(const Dog& d)
{
name = new char[20];
// 只拷贝内容
strcpy(name,d.name);
}
// 析构函数
~Dog()
{
delete name;
}
void show_name()
{
cout << name << endl;
}
};
int main()
{
char c[20] = "wangcai";
Dog d1(c); // d1.name → 堆区1
Dog d2(d1); // d2.name → 堆区2
strcpy(c,"tiedan");
d1.show_name(); // wangcai
d2.show_name(); // wangcai
return 0;
}
7. 作用域限定符 ::
7.1 名字空间
std是C++标准库的一个名字空间,很多使用的内容都是来自于标准名字空间,例如字符串std::string、std::cout...
当项目中包含using namespace std;时,代码中使用std名字空间中的内容就可以省略前面的std::
#include <iostream>
using namespace std;
int a = 1;
// 自定义名字空间
namespace my_space
{
int a = 3;
int b = 4;
}
// 使用自定义名字空间
using namespace my_space;
int main()
{
int a = 2;
cout << a << endl; // 2
cout << ::a << endl; // 1
cout << my_space::a << endl; // 3
// 完整写法
std::string s = "hhh";
std::cout << s << std::endl;
// 实际上是my_space::b
cout << b << endl; // 4
return 0;
}
7.2 类内声明,类外定义
对于类中的成员也可以声明定义分离,如果声明定义分离,通常在类内声明,在类外定义,类外的定义需要结合作用域限定符使用。
#include <iostream>
using namespace std;
class Test
{
private:
string str = "随便";
public:
// 类内声明
Test(); // 无参构造
string get_str(); // getter
};
// 类外定义
string Test::get_str()
{
return str; // 也属于类内
}
Test::Test()
{
cout << "构造函数" << endl;
}
int main()
{
Test t;
cout << t.get_str() << endl;
return 0;
}
7.3 与static关键字配合使用
除了可以使用当前类对象调用静态成员函数外,也可以直接使用类名::调用,推荐后者。具体查看10.3节
#include <iostream>
using namespace std;
class Test
{
public:
static int a;
void method()
{
cout << "非静态成员" << endl;
}
static void func()
{
// method(); 错误
cout << a << endl;
}
static void func2(); // 声明
};
int Test::a = 1;
void Test::func2()
{
cout << "静态成员函数" << endl;
}
int main()
{
Test::func();
Test::func2();
Test t;
t.func();
return 0;
}
8. explicit关键字
如果赋值时,刚好赋值运算符右边的数值是左边类的构造函数可以接收的类型,编译器则会自动调用这个构造函数并把赋值运算符右边的数值传入构造函数中,相当于隐式调用了构造函数。
#include <iostream>
using namespace std;
class Cow
{
private:
string name;
public:
Cow(string n):name(n)
{
cout << "生了一头牛:" << name << endl;
}
string get_name()
{
return name;
}
};
int main()
{
int i1(1);
string s = "小花"; // 直接写双引号的字符串实际上是const char[]
Cow c1(s);
int i2 = 2;
s = "小黑";
Cow c2 = s; // 隐式调用构造函数
cout << c1.get_name() << endl;
cout << c2.get_name() << endl;
return 0;
}
有时候在一些参数传递的过程中,隐式构造可能会在程序员无意的情况下触发了构造函数,因此可以在构造函数前添加explicit关键字修饰,屏蔽隐式调用的用法。
9. this指针
9.1 概念
this指针是一个特殊的指针,保存的是当前类的对象首地址。
#include <iostream>
using namespace std;
class Test
{
public:
void test_this()
{
cout << this << endl;
}
};
int main()
{
Test t1;
cout << &t1 << endl; // 0x61fe8b
t1.test_this(); // 0x61fe8b
Test* t2 = new Test;
cout << t2 << endl; // 0xae2350
t2->test_this(); // 0xae2350
delete t2;
return 0;
}
实际上可以通过下面的方法判断this的指向:
this所在的函数是哪个对象的,this指向的就是这个对象。
9.2 原理
在类内调用此类的成员,虽然不用手写this指针,但是编译器都会使用this指针来调用成员,因为成员只能由对象来调用,而this指针指向的就是当前类的对象。
#include <iostream>
using namespace std;
class Student
{
private:
string name;
public:
Student(string s)
{
this->name = s;
}
string get_name()
{
return this->name;
}
void show()
{
cout << this->get_name() << endl;
}
};
int main()
{
Student s("张三");
s.show(); // 张三
return 0;
}
9.3 应用
利用this指针的原理,其应用有:
- 区分重名的成员变量与局部变量
- 链式调用
- 多态传参
9.3.1 区分重名变量
当成员变量与局部变量重名时,可以使用this指针调用成员变量。
#include <iostream>
using namespace std;
class Student
{
private:
string name;
public:
Student(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
};
int main()
{
Student s("张三");
cout << s.get_name() << endl;
return 0;
}
9.3.2 链式调用
如果一个函数的返回值是当前类的引用,那么通常此函数需要返回一个*this,并且此函数支持链式调用。
#include <iostream>
using namespace std;
class Value
{
private:
int data = 0;
public:
int get_data()
{
return data;
}
/**
* @brief add 加法
* @param i 增加的数值
* @return
*/
Value& add(int i)
{
data += i;
return *this; // *this表示的是当前类的对象本身
}
};
int main()
{
Value v;
// 链式调用 0+1+2+3+4+5
v.add(1).add(2).add(3).add(4).add(5);
cout << v.get_data() << endl; // 15
// 相当于
v.add(1);
v.add(2);
v.add(3);
v.add(4);
v.add(5);
cout << v.get_data() << endl; // 30
return 0;
}
9.3.3 多态传参
this指针指向了对象的起始地址,直接上代码:
#include <iostream>
using namespace std;
class A {
public:
A (void) {
cout << "A this " << this << endl;
}
virtual void fool(void)
{
cout << "A::fool " << endl;
}
int m_a;
};
class B {
public:
B (void) {
cout << "B this " << this << endl;
}
virtual void print(void)
{
cout << "B::print " << "this = " << this << endl;
}
int m_b;
};
class C :public A, public B {
public:
C (void)
{
cout << "C this " << this << endl;
}
virtual void print(void)
{
cout << "C::print " << "this = " << this << endl;
}
int m_c;
};
int main(void){
C c;
A* pa = &c;
B* pb = &c;
C* pc = &c;
cout << "pa = " << pa
<< " pb =" << pb
<< " pc= " << pc
<< endl;
/*调用成员函数print需要两个必备条件:1、成员函数指针 2、正确的this指针
条件1,成员函数指针通过虚表得到
条件2,pb是基类对象指针,pb->print()得到子类函数指针:编译器通过pb基类对象向下造型出一个子类this指针来匹配子类成员函数*/
pb->print();
pc->print();
return 0;
}
输出:A this 0x7fff1e437e50
B this 0x7fff1e437e60
C this 0x7fff1e437e50
pa = 0x7fff1e437e50 pb =0x7fff1e437e60 pc= 0x7fff1e437e50
C::print this = 0x7fff1e437e50
C::print this = 0x7fff1e437e50
10. static关键字
static关键字在类内有以下几种用法:
- 静态局部变量
- 静态成员变量
- 静态成员函数
10.1 静态局部变量
使用static关键字修饰局部变量就是静态局部变量。
#include <iostream>
using namespace std;
class Test
{
public:
void test_static()
{
int a = 1;
// 静态局部变量
static int b = 1;
cout << ++a << " " << ++b << endl;
cout << &a << " " << &b << endl;
}
};
int main()
{
Test t1;
t1.test_static();
cout << endl;
t1.test_static();
return 0;
}
运行结果如下:
虽然上面的结果中,变量a和变量b的地址始终都没有变化,但是变量a在这个地址上创建了两次,而变量b只在此函数第一次调用时创建了一次。
实际上,此类中所有的对象都共享一个静态变量,把上面的代码主函数更改为:
int main()
{
Test t1;
t1.test_static();
cout << endl;
Test t2;
t2.test_static();
return 0;
}
运行结果不变。
静态局部变量所在的函数第一次被调用时,静态局部变量创建,在程序结束时才销毁。
10.2 静态成员变量
成员变量使用static修饰就是静态成员变量,静态成员变量具有以下特点:
- 此类的所有对象共用此变量
- 非const的静态成员变量通常需要类内声明,类外初始化
- 静态成员变量可以直接使用类名::来调用,更推荐使用此方式
- 静态成员变量在程序运行时创建,在程序结束时销毁
#include <iostream> using namespace std; class Test { public: static int a; // 类内声明 }; int Test::a = 1; // 类外初始化 int main() { cout << Test::a << " " << &Test::a << endl; // 1 0x408004 Test t1; cout << ++t1.a << " " << &t1.a << endl; // 2 0x408004 Test t2; cout << t2.a << " " << &t2.a << endl; // 2 0x408004 return 0; }
10.3 静态成员函数
成员函数使用static修饰就是静态成员函数,静态成员函数的特点有:
- 静态成员函数不能访问此类中非静态成员,因为没有this指针
- 静态成员函数只能调用本类中静态的成员
- 非静态成员函数可以调用静态成员
- 除了可以使用当前类对象调用静态成员函数外,也可以直接使用类名::调用,推荐后者
- 如果静态成员函数声明与定义分离,只需要在声明处使用static修饰
#include <iostream> using namespace std; class Test { public: static int a; void method() { cout << "非静态成员" << endl; } static void func() { // method(); 错误 cout << a << endl; } static void func2(); // 声明 }; int Test::a = 1; void Test::func2() { cout << "静态成员函数" << endl; } int main() { Test::func(); Test::func2(); Test t; t.func(); return 0; }
11. const关键字
在C++中,虽然认为const表示常量的意思,但是严格地讲,const并不是常量。因为C++中const只能在程序的运行期间只读,即在编译期可以改变数值。
11.1 常成员函数
const修饰的成员函数,表示常成员函数,这种函数的特点是:
- 可以调用本类中非const的成员变量,但是不能修改其数值
- 不能调用非const的成员函数
建议成员函数只要不修改成员变量值就写为常成员函数,例如getter
#include <iostream>
using namespace std;
class Test
{
private:
int a = 1;
public:
void set_a(int a)
{
this->a = a;
}
int get_a() const
{
return a;
}
void func() const // 常成员函数
{
// a++; 错误
cout << a << endl;
// set_a(6); 错误
cout << get_a() << endl;
}
};
int main()
{
Test t;
t.func();
return 0;
}
11.2 常量对象
const修饰对象,表示该对象为常量对象,其特点有:
- 常量对象的任何属性值不能被修改
- 常量对象不能调用任何非const的成员函数
const修饰对象时,const关键字可以写在类名前面,也可以类名后面。
#include <iostream>
using namespace std;
class Test
{
public:
int a = 1;
void set_a(int a)
{
this->a = a;
}
int get_a() const
{
return a;
}
};
int main()
{
// 常量对象
const Test t1;
Test const t2;
cout << t1.a << endl; // 1
// cout << ++t2.a << endl; 错误
cout << t1.get_a() << endl; // 1
// t2.set_a(2); 错误
return 0;
}
11.3 常成员变量
使用const修饰成员变量,表示该成员变量为常成员变量,其特点有:
- 程序运行时,常成员变量的值不可变
- 不能在函数体中赋值,只能通过直接赋值或构造初始化列表赋值
#include <iostream> using namespace std; class Test { public: const int a = 1; // 直接赋予初始值 const int b; const int c = 3; Test(int b):b(b){} // 构造初始化列表赋值 Test(int b,int c):b(b),c(c){} }; int main() { Test t(2); cout << t.a << endl; // 1 // t.a++; 错误 cout << t.b << endl; // 2 // t.b++; 错误 Test t2(4,4); cout << t2.c << endl; // 4 return 0; }
11.4 修饰局部变量
类似于之前给引用参数增加const修饰,函数的局部变量都可以使用const修饰,表示常量。
#include <iostream>
using namespace std;
class Test
{
public:
void func(const int a)
{
cout << a << endl;
// a++; 错误
const int i = 1;
cout << i << endl;
// i++; 错误
}
};
int main()
{
Test t;
t.func(2);
return 0;
}