三、面向对象基础
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(){} // 编译器自动添加的无参构造函数
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.3 重载
构造函数也支持函数重载,遵守之前函数重载的规则。
#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.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关键字配合使用
在后续static关键字章节中讲解。
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 多态传参
需要在Qt课程中得到解答。
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;
}