重点:面试考试大概率会涉及,需要不借助任何资料掌握。
掌握:面试考试可能涉及,需要不借助任何资料掌握
熟悉:面试考试可能涉及,可以稍微参考资料掌握
了解:面试考试小概率涉及,能吹吹就行。
一、C++简介
1. 为什么要学习C++?
C++是一个新的就业方向
2. C++可以做什么?
- 嵌入式系统级开发。
- 服务器开发、包括游戏服务器、推荐服务器、运营系统(数据处理、系统安全)
- 人工智能
- QT代码实现
拓展知识面 :C语言属于面向过程编程,C++是属于面向对象编程。
3. C++语言的发展史(了解)
1983年,贝尔实验室(Bell Labs)的Bjarne Stroustrup发明了C++。 C++在C语言的基础上进行了扩充和完善,是一种面向对象程序设计(OOP)语言。
Stroustrup说:“这个名字象征着源自于C语言变化的自然演进”。还处于发展完善阶段时被称为“new C”,之后被称为“C with Class”。C++被视为C语言的上层结构,1983年Rick Mascitti建议使用C++这个名字,就是源于C语言中的“++”操作符(变量自增)。而且在共同的命名约定中,使用“+”以表示增强的程序。
常用于系统开发,引擎开发、嵌入式开发等应用领域, 至今仍然是最受广大程序员喜爱的编程语言之一。
4. C++特点(了解)
- 在支持C语言的基础上,全面支持面向对象编程。
- 编程领域广泛,功能强大(最难学习的编程语言之一)。
- C++语言的标准一直保持更新,本次课程主要以C++ISO98 和ISO11标准为主。
- 为数不多支持底层操作的面向对象编程语言。
- 在面向对象的编程语言中执行效率极高。
5. 面向对象编程
- 类
- 对象
- 封装
- 继承
- 多态
封装→继承→多态,被称为面向对象编程的三大特性
6. 面向过程与对象的区别(熟悉)
【思考】如果把大象装进冰箱,需要几步完成?
- (我)打开冰箱
- (我)把大象放进去
- (我)关上冰箱
上面的这种方式就是典型的面向过程的编程思想,这种思想关注的重点是“过程”,“过程”指的是一系列有序的步骤,只需要按照这个步骤来做,就可以得到预计的结果。这种思维方式偏向于计算机执行命令的本质,这种程序的特点就是执行效率高。(因为都是亲历亲为)。适合小体量的软件工程项目开发,偏向性能的项目一般这样做。
使用面向对象的思想把大象装进行冰箱:
- (我)把大象和冰箱拟人化
- (我)给大象和冰箱安排任务
- 大象和冰箱执行任务
面向对象的语言,关注的重点是“对象”。在计算机中“对象”可以理解为一系列由于某种联系而聚焦在一起的数据,在编程的过程中处理对象之间的关系,这种思想更接近于人类的思考方式。这种程序的执行效率低,但是编程效率高。适合大规模的软件项目。
面向过程,是以“怎么解决问题”为核心;
面向对象,是以“谁来解决问题”为核心。
7. 开发环境
可以看到在项目中创建并包含了两个文件:
.pro文件为项目配置文件,通常无需手动编辑,只有在项目中完全开启C++11功能时,增加下面这句话即可。
QMAKE_CXXFLAGS += -std=c++11
添加完成后别忘记保存
.cpp文件为C++的源代码文件,用于编写C++代码
// 头文件,标准输入输出流
// C++源码中,头文件.h以便于与C语言做区分。
#include <iostream>
// 使用标准名字空间(不要删)
using namespace std;
// 主函数,程序的入口
int main()
{
// 连续输出一个字符串和一个换行符
cout << "Hello World!" << endl;
// 返回一个0
return 0;
}
- Alt + 0 显式/隐藏边栏(有些电脑可能是win+0)
- ctrl+A全选,再ctrl+i对齐。 代码排版
- ctrl+F搜索+替换
- ctrl+R运行项目
二、从C到C++
本章介绍一些C++拓展的非面向对象的功能。
1. 引用(掌握)
1.1 概念
引用从一定程度上来说是指针的平替。几乎被所有的面向对象编程语言所使用。引用相当于对某一目标变量起一个“别名”。
操作引用与操作原变量完全一样
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// b是a的引用
int &b = a;
cout << a << " " << &a << endl; // 1 0x61fe88
cout << b << " " << &b << endl; // 1 0x61fe88
return 0;
}
1.2 引用的性质
可以改变引用的值,不能再次成为其他变量的引用
声明引用先初始化不为空
可以将变量的地址赋值给指针,指针指向原来的变量
1.可以改变引用的值,但是不能再次成为其他变量的引用
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// b是a的引用
int &b = a;
int c = 3;
b = c; // 3 只是赋值,b还是a的引用
cout << a << " " << &a << endl; // 3 0x61fe88
cout << b << " " << &b << endl; // 3 0x61fe88
cout << c << " " << &c << endl; // 3 0x61fe84
return 0;
}
2.声明引用时,必须要初始化。
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// int &b; // 错误,引用必须初始化
// b = a;
return 0;
}
3.声明引用时,不能初始化为null
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// int &b = NULL; // 错误,引用不能初始化为NULL
// int &b = nullptr; // 错误,引用不能初始化为nullptr
return 0;
}
4.引用在声明时,初始化的值可以是纯数值,但是此时要用const关键字修饰引用,表示该引用为常量引用,这样的引用的值不可变
#include <iostream>
using namespace std;
int main()
{
const int &b = 123; // 常量引用
// b = 22; // 错误常量引用的值不能被改变
cout << b << " " << &b << endl;
return 0;
}
5.可以将变量引用的地址赋值给一个指针,此时指针指向的还是原来的变量
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int &b = a;
int *c = &b; // c同时指向了A和B
cout << a << " " << &a << endl; // 1 0x61fe84
cout << b << " " << &b << endl; // 1 0x61fe84
cout << *c << " " << c << endl; // 1 0x61fe84
return 0;
}
6.可以使用const修饰引用,此时如果原变量的值改变,引用的值也改变。
#include <iostream>
using namespace std;
int main()
{
int a = 2;
const int &b = a;
// b++; // 错误 b是只读的
a++;
cout << a << " " << &a << endl; // 3 0x61fe88
cout << b << " " << &b << endl; // 3 0x61fe88
const int c = 2;
const int &d = c;
cout << c << " " << &c << endl; // 2 0x61fe80
return 0;
}
1.3 引用的参数
【思考】
写一个函数,函数有两个参数a和b,函数的功能交换两个传入参数原来变量的值。
#include <iostream>
using namespace std;
// 需求:写一个函数,实现参数a和b的数据交换。
// 值传递,操作的是拷贝的副本,不符合需求
void test(int a1,int b1)
{
int temp = a1;
a1 = b1;
b1 = temp;
cout << "a1 =" << a1 << endl;
cout << "b1 =" << b1 << endl;
}
// C语言,不推荐这种写法
void test1(int *a1,int *b1)
{
*a1 = *a1 ^ *b1;
*b1 = *a1 ^ *b1;
*a1 = *a1 ^ *b1;
}
// 引用传递不会产生副本。C++编程方式,符合需求
void test2(int &a1,int &b1)
{
a1 = a1 ^ b1;
b1 = a1 ^ b1;
a1 = a1 ^ b1;
}
int main()
{
int a = 1;
int b = 2;
test(a,b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
test1(&a,&b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
test2(a,b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
return 0;
}
引用作为参数进行定义的时候,在参数传递时,是不会产生副本的,这样会提高运行效率,我们编程中,建议使用引用进行传递参数。
引用参数,在不参数计算的情况下,我们建议使用const进行修饰,以达到引用安全的目的。
2. 赋值(熟悉)
通常编程中使用=进行赋值操作,C++新增了一些赋值语法。
#include <iostream>
using namespace std;
int main()
{
int a(1); // int a = 1;
cout << a << endl;
int b(a);
cout << b << endl; // int b = a;
int c(a+b);
cout << c << endl; // int c = a + b;
return 0;
}
在C++11中对上述写法又进行升级 大括号会对数据窄化做出警告。
3. 键盘输入(熟悉)
可以使用cin把用户在命令行中输入的内容赋值到变量中。
cin与cout一样,都是属于头文件iostream中的标准输入输出流。
#include <iostream>
using namespace std;
int main()
{
int a;
// C++的字符串
string str;
cout << "请输入一个数字和字符串" << endl;
cin >> a >> str; // 接收键盘输入,一个整数和字符串,可以连续操作
cout << a << " " << str << endl;
return 0;
}
如果cin输入的字符串需要包含空格,则可以使用下面的方式:
#include <iostream>
using namespace std;
int main()
{
string a; //C++的字符串类型
cout << "请输入一个字符串,可以包含空格,输入完成后按下回车" << endl;
getline(cin,a);
cout << "您输入的是:" << a << endl;
return 0;
}
4. string字符串类(掌握)
string不是C++的基本数据类型,是一个C++标准库中的字符串类,使用时需要引入头文件#include<string>,而不是string.h
string在绝大多数情况下可以代替C语言中的字符串,不必担心内存是否足够和字符串长度等问题,其中内部还包含了很多字符串处理函数,可以完成各种情况下的字符串处理功能。
string和C语言相同,字符串编码也是使用ASCII编码,不支持中文。
#include <iostream>
using namespace std;
int main()
{
string str = "helloworld";
cout << str.size() << endl; // 10
cout << str.length() << endl; // 10
cout << str[0] << endl; // h
cout << str.at(1) << endl; // e
return 0;
}
两种方式都可以,但是在C++中推荐使用at函数,原因是更加的安全。但是[ ]的方式效率更高
#include <iostream>
using namespace std;
int main()
{
string str = "helloworld";
// cout << str[100] << endl; // 输出一个越界数据
// cout << "hello" << endl;
cout << str.at(100) << endl; // 程序运行停止
cout << "hello" <<endl;
return 0;
}
string类支持很多种遍历方式
- 普通循环(for)
- C++11 for each循环
#include <iostream>
using namespace std;
int main()
{
string str = "helloworld";
for(int i = 0; i < str.size(); ++i)
{
cout << str.at(i);
}
cout << endl;
// 以 for each的方式进行循环遍历字符串
for(char i:str)
{
cout << i;
}
return 0;
}
5. 函数
5.1 内联函数(熟悉)
内联函数用于取代C语言中宏定义的函数,内联函数的正确使用可以体征程序的执行效率,内联函数在编译的时候,直接把函数体展开到主函数中进行编译,在运行时可以减少调用的开销。
空间换时间
通常讲具有以下性质的函数写为内联函数:
- 代码长度5行以内。
- 不包含复杂的控制语句
- 调用频繁
#include <iostream>
using namespace std;
// 内联函数
inline void print_string(string str)
{
cout << str << endl;
}
int main()
{
print_string("helloworld");
return 0;
}
但是我们手动添加上inline关键字,将函数声明为内联函数。这个不是我们能决定的,编译器有自己的判断准则,我们只是给编译器提一个建议。具体是否变为内联函数,还是编译器自己决定的。
后续学习的成员函数默认都添加了inline修饰。
5.2 函数重载overload(重点)
C++中允许多个函数使用同一个名称,这种用法就是函数重载,函数重载要求函数名称相同
但是参数不同(类型或者数量)或者前后顺序不同。与返回值等其他因素无关
#include <iostream>
using namespace std;
// 错误名称一样,参数类型一样,编译器无法区分
//double print_show(const int s)
//{
// cout << "调用了int重载:" << s << endl;
// return 1.2;
//}
void print_show(string s)
{
cout << "调用了string重载:" << s << endl;
}
//void print_show(float f)
//{
// cout << "调用了float重载:" << f << endl;
//}
void print_show(int i)
{
cout << "调用了int重载:" << i << endl;
}
int main()
{
print_show(1);
return 0;
}
5.3 哑元函数(熟悉)
函数的参数只有类型,没有名称,有这样的参数的函数就是哑元函数。
#include <iostream>
using namespace std;
// 哑元函数
void print_show(int)
{
cout << "调用了int函数" << endl;
}
// 错误 哑元函数无法区分函数重载
//void print_show(int i)
//{
// cout << "调用了int i 函数" << endl;
//}
int main()
{
print_show(1);
return 0;
}
作用1:哑元函数用来区分函数重载
作用2:运算符重载中用到
#include <iostream>
using namespace std;
// 哑元函数
void print_show(int i,int)
{
cout << "调用了int函数" << endl;
}
// 错误 哑元函数无法区分函数重载
void print_show(int i)
{
cout << "调用了int i 函数" << endl;
}
int main()
{
print_show(1);
return 0;
}
1、键盘输入一个100-999之间的数,依次输出这个数的个十百位。
2、输入一行字符串,分别统计出其中的英文字母,数字和其他字符的个数。
#include <iostream>
using namespace std;
#if 0
int main()
{
int a;
cout<<"请输入一个100-999之间的数"<<endl;
cin>>a;
if(a>100&&a<999)
{
int a1=a/100;
int a2=a/10%10;
int a3=a%10;
cout<<"百位:"<<a1<<" 十位:"<<a2<<" 个位:"<<a3<<endl;
}
else
{
cout<<"输入错误"<<endl;
}
return 0;
}
#endif
#if 1
int main()
{
string str;
int sum1=0,sum2=0,sum3=0;
cout<<"请输入一个字符串:"<<endl;
getline(cin,str);
for(int i=0;i<str.size();++i)
{
if(str.at(i)>=48&&str.at(i)<=57)
{
sum1++;
}
else if((str.at(i)>=65&&str.at(i)<=90)||(str.at(i)>=97&&str.at(i)<=122))
{
sum2++;
}
else
{
sum3++;
}
}
cout<<"数字个数:"<<sum1<<endl;
cout<<"英文字母个数:"<<sum2<<endl;
cout<<"其他字符个数:"<<sum3<<endl;
return 0;
}
#endif
三、面向对象基础
1. 类与对象
1.1 概念
类:抽象的概念,描述同一类对象的特点
对象:根据类的概念所创造的实体
一个对象可以没有对应类嘛?
不可以,因为必须先写类才能创建对象。
1.2 类的内容
类中最基础的内容包括两部分:一个是属性,一个是行为。
属性:表示一些特征项的数值,比如说:身高、体重、颜色、型号等等。而这些特征项的数值也被称为“成员变量”。属性一般以名词存在。
行为:表示能执行的动作,能干什么事?比如说:吃饭、睡觉、打架、爬山、唱、跳、rap、篮球。行为一般通过函数实现,也被称为“成员函数”。行为一般以动词存在。
成员 = 成员函数+成员变量。
#include <iostream> // 输入输出流库,用于输入和输出数据
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
public: // 权限:public最开放的权限,表示可以在类的外部访问
string brand; // 手机品牌
string model; // 手机型号
int weight; // 手机重量(克)
// 播放音乐
void play_music()
{
cout << "只因你太美,哒哒哒" << endl; // 输出播放音乐的提示信息
}
// 运行游戏
void run_game()
{
cout << "植物大战僵尸杂交版" << endl; // 输出运行游戏的提示信息
}
// 打电话
void call()
{
cout << "哥哥~ 您在吗?" << endl; // 输出打电话的提示信息
}
};
int main()
{
return 0; // 返回0,表示程序正常结束
}
1.3 对象的创建
C++中 存在两种类型的对象。
栈内存对象
手动创建,对象所在的{ }执行完毕后,自动被销毁。
#include <iostream> // 输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法
class MobilePhone {
public:
string brand; // 手机品牌
string model; // 手机型号
int weight; // 手机重量(克)
// 播放音乐
void play_music() {
cout << "听音乐" << endl;
}
// 玩游戏
void play_game() {
cout << "玩游戏" << endl;
}
// 打电话
void call() {
cout << "打电话" << endl;
}
};
int main() {
MobilePhone mp; // 创建 MobilePhone 类的对象
mp.brand = "小米"; // 设置手机品牌为小米
mp.model = "14ultra"; // 设置手机型号为 14ultra
mp.weight = 200; // 设置手机重量为 200 克
// 输出手机信息
cout << "品牌:" << mp.brand << " 型号:" << mp.model << " 重量:" << mp.weight << "克" << endl;
// 执行手机功能
mp.play_music(); // 播放音乐
mp.play_game(); // 玩游戏
mp.call(); // 打电话
return 0;
}
堆内存创建
必须使用new关键字创建,使用指针保存,delete关键字销毁,如果不销毁,则堆内存对象会持续存在。而堆内存泄漏。堆内存调用对象时,使用 -> 而不是.
#include <iostream> // 输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法
class MobilePhone {
public:
string brand; // 手机品牌
string model; // 手机型号
int weight; // 手机重量(克)
// 播放音乐
void play_music() {
cout << "听音乐" << endl;
}
// 玩游戏
void play_game() {
cout << "玩游戏" << endl;
}
// 打电话
void call() {
cout << "打电话" << endl;
}
};
int main() {
MobilePhone *mp = new MobilePhone; // 创建 MobilePhone 类的对象并分配内存
mp->brand = "三星"; // 设置手机品牌为三星
mp->model = "Galaxy S23"; // 设置手机型号为 Galaxy S23
mp->weight = 200; // 设置手机重量为 200 克
// 输出手机信息
cout << "品牌:" << mp->brand << " 型号:" << mp->model << " 重量:" << mp->weight << "克" << endl;
// 执行手机功能
mp->play_music(); // 播放音乐
mp->play_game(); // 玩游戏
mp->call(); // 打电话
delete mp; // 释放分配的内存
mp = NULL; // 将指针置为空
return 0;
}
//在主函数 main 中,动态分配了一个 MobilePhone 对象,设置了其品牌、型号和重量属性,然后调用了其方法,最后释放了动态分配的内存。
2. 封装
在上一节中的手机类与结构体差别不大,实际上可以认为结构体就是一种完全开放的类。
封装指的是,将类的一些属性和细节隐藏,重新对外提供访问函数,封装可以提升代码的安全性,并且可以让程序员更关注上层架构而非内部细节。
#include <iostream> // 输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
private: // 私有权限:private最封闭的权限,只能在类内进行访问
string brand; // 手机品牌
string model; // 手机型号
int weight; // 手机重量(克)
public: // 权限:public最开放的权限
// get 读函数
string get_brand() // 获取手机品牌
{
return brand; // 返回手机品牌
}
// set 写函数
void set_brand(string b) // 设置手机品牌
{
brand = b; // 将参数 b 赋值给手机品牌
}
};
int main()
{
MobilePhone mp1; // 创建 MobilePhone 类的对象 mp1,在栈上分配内存
mp1.set_brand("华为"); // 调用 set_brand 方法设置手机品牌为 "华为"
cout << mp1.get_brand() << endl; // 调用 get_brand 方法获取手机品牌并输出
MobilePhone *mp = new MobilePhone; // 创建 MobilePhone 类的对象 mp,在堆上分配内存
mp->set_brand("菠萝手机"); // 调用 set_brand 方法设置手机品牌为 "菠萝手机"
cout << mp->get_brand() << endl; // 调用 get_brand 方法获取手机品牌并输出
delete mp; // 释放动态分配的内存
mp = NULL; // 将指针置为空
return 0; // 返回0,表示程序正常结束
}
3. 构造函数
构造函数是一种特殊的成员函数,用于创建对象时初始化,写法上有以下要求:
- 函数名称必须与类名完全相同
- 构造函数不写返回值
- 如果程序员不手动编写构造函数,编译器会自动添加一个无参的构造函数,手动添加构造函数后,编译器就不会自动添加默认无参构造函数了。
3.1 基本使用
构造函数在创建对象时,常用于给对象的属性赋予初始值。
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
private: // 私有权限:private 最封闭的权限,只能在类内进行访问
string brand; // 品牌
string modle; // 型号
int weight; // 重量
public: // 公有权限:public 最开放的权限,可以在类外部访问
// 编译器自动添加的构造函数(无参构造函数)
MobilePhone()
{
brand = "8848"; // 默认品牌为 "8848"
modle = "M6巅峰版"; // 默认型号为 "M6巅峰版"
weight = 888; // 默认重量为 888
}
// get 读函数,用于获取品牌
string get_brand()
{
return brand; // 返回品牌
}
// set 写函数,用于设置品牌
void set_brand(string b)
{
brand = b; // 设置品牌
}
};
int main()
{
MobilePhone mp1; // 在栈内存中创建一个 MobilePhone 对象 mp1
cout << mp1.get_brand() << endl; // 调用对象的 get_brand() 方法,打印品牌信息
return 0; // 返回 0,表示程序成功结束
}
造函数也支持重载
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
private: // 私有权限:private 最封闭的权限,只能在类内进行访问
string brand; // 品牌
string modle; // 型号
int weight; // 重量
public: // 公有权限:public 最开放的权限,可以在类外部访问
// 编译器自动添加的构造函数(无参构造函数)
MobilePhone()
{
brand = "8848"; // 默认品牌为 "8848"
modle = "M6巅峰版"; // 默认型号为 "M6巅峰版"
weight = 888; // 默认重量为 888
}
// 有参构造函数
MobilePhone(string b, string m, int w)
{
brand = b; // 使用传入的参数值初始化品牌
modle = m; // 使用传入的参数值初始化型号
weight = w; // 使用传入的参数值初始化重量
}
// get 读函数,用于获取品牌
string get_brand()
{
return brand; // 返回品牌
}
// set 写函数,用于设置品牌
void set_brand(string b)
{
brand = b; // 设置品牌
}
};
int main()
{
// 在栈内存中创建一个 MobilePhone 对象 mp1,并使用有参构造函数初始化
MobilePhone mp1("金立", "老年机", 400);
cout << mp1.get_brand() << endl; // 调用对象的 get_brand() 方法,打印品牌信息
// 在栈内存中创建一个 MobilePhone 对象 mp2,并使用无参构造函数初始化
MobilePhone mp2;
cout << mp2.get_brand() << endl; // 调用对象的 get_brand() 方法,打印品牌信息
// 在堆内存中动态创建一个 MobilePhone 对象,使用无参构造函数初始化,并返回指向该对象的指针
MobilePhone *mp3 = new MobilePhone;
cout << mp3->get_brand() << endl; // 通过指针调用对象的 get_brand() 方法,打印品牌信息
delete mp3; // 释放动态分配的内存
// 在堆内存中动态创建一个 MobilePhone 对象,使用有参构造函数初始化,并返回指向该对象的指针
MobilePhone *mp4 = new MobilePhone("1加", "Ace", 100);
cout << mp4->get_brand() << endl; // 通过指针调用对象的 get_brand() 方法,打印品牌信息
delete mp4; // 释放动态分配的内存
return 0; // 返回 0,表示程序成功结束
}
构造函数也支持函数默认值参数
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
private: // 私有权限:private 最封闭的权限,只能在类内进行访问
string brand; // 品牌
string modle; // 型号
int weight; // 重量
public: // 公有权限:public 最开放的权限,可以在类外部访问
// 有参构造函数,带有默认参数值
MobilePhone(string b = "oppo", string m = "锤子", int w = 200)
{
brand = b; // 使用传入的参数值初始化品牌,如果没有传入参数,则使用默认值 "oppo"
modle = m; // 使用传入的参数值初始化型号,如果没有传入参数,则使用默认值 "锤子"
weight = w; // 使用传入的参数值初始化重量,如果没有传入参数,则使用默认值 200
}
// get 读函数,用于获取品牌
string get_brand()
{
return brand; // 返回品牌
}
// set 写函数,用于设置品牌
void set_brand(string b)
{
brand = b; // 设置品牌
}
};
int main()
{
// 创建一个 MobilePhone 对象 mp2,没有传入参数,使用默认参数值初始化
MobilePhone mp2;
cout << mp2.get_brand() << endl; // 调用对象的 get_brand() 方法,打印品牌信息
return 0; // 返回 0,表示程序成功结束
}
3.2 构造初始化列表
构造初始化列表是一种更简单的给成员变量赋予初始值的方式。
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
// 帕斯卡命名法(大驼峰命名法)
// 每个单词的首字母要大写
class MobilePhone
{
private: // 私有权限:private 最封闭的权限,只能在类内进行访问
string brand; // 品牌
string modle; // 型号
int weight; // 重量
public: // 公有权限:public 最开放的权限,可以在类外部访问
// 无参构造函数
MobilePhone() : brand("8848"), modle("M6巅峰版"), weight(888)
{
cout << "无参构造函数" << endl; // 输出提示信息
}
// 有参构造函数
MobilePhone(string b, string m, int w)
: brand(b), modle(m), weight(w) // 使用初始化列表初始化成员变量
{
cout << "有参构造函数" << endl; // 输出提示信息
}
// get 读函数,返回品牌
string get_brand()
{
return brand; // 返回品牌
}
// set 写函数,设置品牌
void set_brand(string b)
{
brand = b; // 设置品牌
}
};
int main()
{
// 使用有参构造函数创建对象 mp2
MobilePhone mp2("小米", "红米", 200);
// 输出 mp2 的品牌
cout << mp2.get_brand() << endl;
// 使用无参构造函数创建对象 mp3
MobilePhone mp3;
// 输出 mp3 的品牌
cout << mp3.get_brand() << endl;
return 0; // 返回 0,表示程序成功结束
}
当构造函数的局部变量与成员变量重名时,除了使用后面学习的this指针的方式外,也可以使用构造初始化列表的方式区分。
// 使用初始化列表区分成员变量和参数
MobilePhone(string brand, string modle, int weight)
: brand(brand), modle(modle), weight(weight) {
cout << "有参构造函数" << endl;
}
3.3 隐式调用构造函数(熟悉)
构造函数的调用方式分为显式调用和隐式调用。
显式调用指的是在创建对象时手写构造函数的名称。
隐式调用构造函数指的是创建对象时不写构造函数名称和参数列表,编译器会尝试调用对应参数的构造函数。
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
class Student
{
private:
int age; // 私有成员变量,表示学生的年龄
public:
// 显式构造函数,使用 explicit 关键字
explicit Student(int a) : age(a)
{
cout << "构造函数" << endl; // 在构造函数中输出提示信息
}
// 获取学生年龄的成员函数
int get_age()
{
return age; // 返回学生的年龄
}
};
int main()
{
// 显式调用构造函数创建一个 Student 对象 s1,传入参数为 1
Student s1(1);
cout << s1.get_age() << endl; // 打印 s1 对象的年龄
// 显式调用构造函数创建一个临时的 Student 对象,传入参数为 14,并将其赋值给 s3 对象
Student s3 = Student(14);
cout << s3.get_age() << endl; // 打印 s3 对象的年龄
// 隐式调用构造函数创建一个临时的 Student 对象,传入参数为 15,并将其赋值给 s4 对象
Student s4 = 15;
cout << s4.get_age() << endl; // 打印 s4 对象的年龄
// 显式调用构造函数创建一个动态分配的 Student 对象 s2,传入参数为 13
Student *s2 = new Student(13);
cout << s2->get_age() << endl; // 打印 s2 指向的对象的年龄
delete s2; // 释放动态分配的内存
s2 = NULL; // 将指针置为空指针,避免出现悬空指针的情况
return 0; // 返回 0,表示程序成功结束
}
建议使用显式调用,可以使用explicit关键字来屏蔽隐式调用语法。
3.4 构造拷贝函数
当程序员不手写拷贝构造函数时,编译器会自动添加一个拷贝构造函数,使对象创建可以通过这个构造函数进行实现。
#include <iostream> // 包含输入输出流库
using namespace std; // 使用标准命名空间
class MobilePhone { // 定义一个名为 MobilePhone 的类
private:
string brand; // 私有成员变量,表示手机品牌
string modle; // 私有成员变量,表示手机型号
int weight; // 私有成员变量,表示手机重量
public:
// 使用初始化列表的构造函数,用于初始化手机品牌、型号和重量
MobilePhone(string brand, string modle, int weight)
: brand(brand), modle(modle), weight(weight) {} // 初始化列表
// 手动编写的拷贝构造函数
MobilePhone(const MobilePhone &mp) {
brand = mp.brand; // 将拷贝对象的 brand 值赋给新对象的 brand
modle = mp.modle; // 将拷贝对象的 modle 值赋给新对象的 modle
weight = mp.weight; // 将拷贝对象的 weight 值赋给新对象的 weight
}
// 成员函数,用于显示手机信息
void show() {
cout << brand << " " << modle << " " << weight << " " << endl; // 输出手机品牌、型号和重量
}
};
int main() {
// 创建一个 MobilePhone 对象 mp1,并初始化为 "vivo", "iqoo", 230
MobilePhone mp1("vivo", "iqoo", 230);
// 使用拷贝构造函数创建一个新的 MobilePhone 对象 mp2,拷贝自 mp1
MobilePhone mp2(mp1);
// 调用 mp2 的 show 函数,显示 mp2 的信息
mp2.show();
return 0; // 返回 0,表示程序成功结束
}
思考:拷贝构造函数会存在隐患嘛?
存在,当成员变量出现指针类型时,默认的拷贝构造函数会导致两个对象的成员变量指向同一处,不符合面向对象的设计规范,这种现象被称为“浅拷贝”。
#include <iostream> // 包含输入输出流库
#include <string.h> // 包含字符串操作函数库
using namespace std; // 使用标准命名空间
class Dog
{
private:
char *name; // 用于存储狗的名字的指针
public:
// 有参构造函数
Dog(char *n)
{
name = n; // 将参数 n 的地址赋值给 name,浅拷贝,只是复制指针的值
}
// 显示狗的名字和指针地址
void show_name()
{
cout << name << endl; // 打印名字
cout << (int *)name << endl; // 打印指针地址
}
};
int main()
{
char arr[20] = "旺财"; // 创建一个字符数组并初始化为 "旺财"
Dog d1(arr); // 使用字符数组创建一个 Dog 对象 d1
Dog d2(d1); // 使用拷贝构造函数创建另一个 Dog 对象 d2
strcpy(arr, "大黄"); // 将字符数组的内容改为 "大黄"
d1.show_name(); // 打印 d1 的名字和指针地址
d2.show_name(); // 打印 d2 的名字和指针地址
return 0; // 返回 0,表示程序成功结束
}
创建对象
d1
:
d1
被初始化时,name
指向arr
数组的首地址。拷贝构造函数创建对象
d2
:
使用浅拷贝,
d2
的name
也指向arr
数组的首地址。修改
arr
的内容:
将
arr
改为 "大黄" 后,d1
和d2
的name
都指向这个数组,因此它们的name
内容都变成了 "大黄"。显示
d1
和d2
的名字和指针地址:
d1.show_name()
和d2.show_name()
打印出的名字都变成了 "大黄"。打印的指针地址是相同的,因为
d1
和d2
的name
都指向arr
。输出结果
大黄
0x7ffdecd4b3c0
大黄
0x7ffdecd4b3c0
这种情况必须手动重写拷贝构造函数,使每次赋值都创建一个新的副本,从而每个对象单独持有自己的成员变量。这种方式被称为“深拷贝”。
#include <iostream> // 包含输入输出流库
#include <string.h> // 包含字符串操作函数库
using namespace std; // 使用标准命名空间
class Dog
{
private:
char *name; // 用于存储狗的名字的指针
public:
// 有参构造函数
Dog(char *n)
{
name = new char[20]; // 为 name 分配动态内存
strcpy(name, n); // 将传入的名字复制到动态分配的内存中
}
// 拷贝构造函数
Dog(const Dog &d)
{
name = new char[20]; // 为新对象的 name 分配动态内存
strcpy(name, d.name); // 将原对象的名字复制到新对象的动态分配的内存中
}
// 显示狗的名字和指针地址
void show_name()
{
cout << name << endl; // 打印名字
cout << (int *)name << endl; // 打印指针地址
}
// 析构函数,用于释放动态分配的内存
~Dog()
{
delete[] name; // 释放动态分配的内存
}
};
int main()
{
char arr[20] = "旺财"; // 创建一个字符数组并初始化为 "旺财"
Dog d1(arr); // 使用字符数组创建一个 Dog 对象 d1
Dog d2(d1); // 使用拷贝构造函数创建另一个 Dog 对象 d2
strcpy(arr, "大黄"); // 将字符数组的内容改为 "大黄"
d1.show_name(); // 打印 d1 的名字和指针地址
d2.show_name(); // 打印 d2 的名字和指针地址
return 0; // 返回 0,表示程序成功结束
}
【思考】深拷贝的代码是否存在隐患?
存在,new开辟的空间无法释放,造成内存泄漏的问题。
4. 析构函数
析构函数是与构造函数对立的函数
构造函数 | 析构函数 |
创建对象时手动调用 | 对象销毁时,自动调用 |
函数名称是类名 | 函数名称是~类名 |
构造函数是可以重载 | 析构函数没有参数,不能重载 |
用于创建对象时并初始化 | 用于销毁对象时释放资源 |
不写返回值 | 没有返回值 |
#include <iostream>
#include <string.h>
using namespace std;
// 定义一个 Dog 类
class Dog
{
private:
char *name; // 用于存储狗的名字的指针
public:
// 有参构造函数,用于初始化 Dog 对象
Dog(char *n)
{
// 为名字分配20个字符的动态内存
name = new char[20];
// 将传入的名字复制到分配的内存中
strcpy(name, n);
}
// 拷贝构造函数,用于初始化一个新的 Dog 对象,使其与现有对象相同
Dog(const Dog &d)
{
// 为名字分配20个字符的动态内存
name = new char[20];
// 将现有对象的名字复制到新对象中
strcpy(name, d.name);
}
// 展示狗的名字
void show_name()
{
// 输出狗的名字
cout << name << endl;
// 输出名字指针的地址
cout << reinterpret_cast<void*>(name) << endl;
}
// 析构函数,用于清理资源
~Dog()
{
cout << "我被调用了" << endl;
// 释放分配的内存
delete [] name;
}
};
int main()
{
// 定义一个字符数组并初始化为 "旺财"
char arr[20] = "旺财";
// 使用 arr 初始化 Dog 对象 d1,调用有参构造函数
Dog d1(arr);
// 使用 d1 初始化 Dog 对象 d2,调用拷贝构造函数
Dog d2(d1);
// 修改字符数组 arr 的内容为 "大黄"
strcpy(arr, "大黄");
// 展示 d1 对象的名字,预期输出 "旺财"
d1.show_name();
// 展示 d2 对象的名字,预期输出 "旺财"
d2.show_name();
// 当 main 函数结束时,d1 和 d2 的析构函数将被自动调用
return 0;
}
5. 作用域限定符
5.1 名字空间(熟悉)
#include <iostream>
using namespace std;
namespace my_space
{
int a = 3;
int b = 4;
}
using namespace my_space;
int a = 2;
int main()
{
int a = 1;
cout << a << endl; // 就近原则,打印1
cout << ::a << endl; // ::是匿名名字空间 2
cout << my_space::a << endl; // 3
cout << b << endl; // 4
return 0;
}
5.2 类内声明,类外定义(掌握)
#include <iostream>
using namespace std;
class Demo
{
public:
Demo();
void test(string str);
};
Demo::Demo()
{
cout << "创建了一个对象" << endl;
}
void Demo::test(string str)
{
cout << str << endl;
}
int main()
{
Demo d;
d.test("hello");
return 0;
}
6.this指针(掌握)
6.1 概念
this指针是一个特殊的指针,指向当前对象的首地址。
成员函数(包括构造函数与析构函数)中都有this指针,因此this指针只能在类内使用。
实际上this指针指向的就是当前运行的成员函数所绑定的对象。
#include <iostream>
using namespace std;
class Test
{
public:
void test_this()
{
cout << this << endl;
}
};
int main()
{
//变量 t1 在栈上分配
Test t1;
cout << &t1 << endl; // 0x61fe8f
t1.test_this(); // 0x61fe8f
//变量 t2 在堆上分配
Test *t2 = new Test;
cout << t2 << endl; // 0xf62740
t2->test_this(); // 0xf62740
delete t2;
return 0;
}
6.2 this指针功能
6.2.1 类内调用成员
成员(变量+函数)必须由对象调用。类中成员的调用都依赖于this指针,通常由编译器自动添加。
#include <iostream>
using namespace std;
class Test
{
private:
string name;
public:
Test(string n)
{
// 编译器默认添加this指针,指向的对象调用成员
this->name = n;
}
void test_this()
{
cout << this << endl;
}
string get_name()
{
return this->name;
}
};
int main()
{
Test t1("zhangsan");
cout << t1.get_name() << endl;
Test t2("lisi");
cout << t2.get_name() << endl;
return 0;
}
6.2.2 区分重名的成员变量和局部变量
#include <iostream>
using namespace std;
class Test
{
private:
string name;
public:
Test(string name):name(name) // 构造初始化列表区分
{
// 通过this指针在函数体中区分
// this->name = name;
}
void test_this()
{
cout << this << endl;
}
string get_name()
{
return this->name;
}
};
int main()
{
Test t1("zhangsan");
cout << t1.get_name() << endl;
Test t2("lisi");
cout << t2.get_name() << endl;
return 0;
}
6.2.3 链式调用
支持链式调用的成员函数特点:
- 当一个成员函数的返回值是当前类型的引用时,往往表示这个函数支持链式调用。
- return后面是*this
#include <iostream>
using namespace std;
class Test
{
private:
int val = 0;
public:
Test& add(int i)
{
val += i; // val = val + i;
return *this; // this是一个指针,需要取内容返回对象的引用
}
int get_val()
{
return val;
}
};
int main()
{
Test t1;
t1.add(1);
t1.add(2);
t1.add(100);
cout << t1.get_val() << endl;
Test t2;
// 链式调用
cout << t2.add(2).add(21).add(200).get_val() << endl;
cout << t2.get_val() << endl;
return 0;
}
7. static关键字(掌握)
7.1 静态局部变量
使用static修饰局部变量,这样的变量就是静态局部变量。
静态局部变量在第一次调用时创建,直到程序结束后销毁,同一个类的所有对象共用这一份静态局部变量。
#include <iostream>
using namespace std;
class Test
{
public:
void func()
{
int a = 1;
static int b = 1; // 静态局部变量
cout << "a=" << ++a << " " << &a << endl;
cout << "b=" << ++b << " " << &b << endl;
}
};
int main()
{
Test t1;
t1.func();
t1.func();
Test t2;
t2.func();
t2.func();
return 0;
}
7.2 静态成员变量
使用static修饰成员变量,这样的变量就是静态成员变量。
静态成员变量需要在类内声明,类外初始化。
一个类的所有对象共用一份静态成员变量,虽然静态成员变量可以使用对象调用,但是更简易直接使用类名调用。所以静态成员变量可以脱离对象使用,在程序开始运行时就开辟内存直到程序运行结束销毁。
更推荐使用类名进行调用,代码的可读性更好。
#include <iostream>
using namespace std;
class Test
{
public:
int a = 1;
// static int b = 2; // 错误,静态成员变量需要类内声明,类外初始化
static int b;
};
int Test::b = 1; // 静态成员变量类外初始化
int main()
{
// 脱离对象使用
cout << Test::b << " " << &Test::b << endl;
Test t1,t2;
cout << t1.a++ << " " << &t1.a << endl; // 1 0x61fe8c
cout << t2.a++ << " " << &t2.a << endl; // 1 0x61fe88
cout << t1.b++ << " " << &t1.b << endl; // 1 0x403004
cout << t2.b++ << " " << &t2.b << endl; // 2 0x403004
cout << Test::b << " " << &Test::b << endl; // 3 0x403004
return 0;
}
7.3 静态成员函数
使用static修饰成员函数,这样的函数就是静态成员函数。
与静态成员变量相似的有:
都可以通过类名直接调用,也可以通过对象调用。(推荐使用类名直接调用)。
都可以脱离对象使用。
静态成员函数没有this指针,不能在静态成员函数中调用同类其他非静态成员,但是静态成员函数可以调用静态成员。
#include <iostream>
using namespace std;
class Test
{
public:
void func()
{
// func1(); // 非静态成员函数可以调用静态成员函数
cout << "非静态成员函数" << endl;
}
static void func1()
{
// func(); // 错误,静态成员函数,不能调用非静态成员函数
cout << "静态成员函数" << endl;
}
static void func2()
{
func1();
cout << "静态成员函数2" << endl;
}
};
int main()
{
// Test::func1();
Test t1;
// t1.func();
t1.func2();
return 0;
}
如果要在静态成员函数内调用非静态成员的属性。可以通过参数将对象传递进来,因为静态成员函数没有this指针。也可以在函数内部创建新的对象。例如:
#include <iostream>
using namespace std;
class Test
{
public:
void func()
{
// func1(); // 非静态成员函数可以调用静态成员函数
cout << "非静态成员函数" << endl;
}
static void func1()
{
Test t2;
t2.func(); // 错误,静态成员函数,不能调用非静态成员函数
cout << "静态成员函数" << endl;
}
static void func2()
{
// func1();
cout << "静态成员函数2" << endl;
}
};
int main()
{
// Test::func1();
Test t1;
t1.func1();
t1.func2();
return 0;
}
7.4 单例设计模式(了解)
设计模式是一套被反复是使用,多人知晓的、经过分类的、代码设计经验的总结。通常用于一些面向对象的语言,如Java、C++、C#等等。
本节以一个简化版本的单例设计模式为例,讲解static的实际使用
#include <iostream>
using namespace std;
// 单例设计模式
class Singleton
{
private:
Singleton(){}
Singleton(const Singleton &sing){}
static Singleton *instance; // 静态成员变量
public:
static Singleton *get_instance()
{
if(instance == NULL)
{
instance = new Singleton;
}
return instance;
}
static void delete_instance()
{
if(instance != NULL)
{
delete instance;
instance = NULL;
}
}
};
Singleton *Singleton::instance = NULL;
int main()
{
Singleton *s1 = Singleton::get_instance();
cout << s1 << endl;
Singleton *s2 = Singleton::get_instance();
cout << s2 << endl;
return 0;
}
8. const关键字(掌握)
8.1 const修饰成员函数
const修饰的成员函数,表示常成员函数。
特性如下:
可以调用成员变量,但是不能修改成员变量的值。
不能调用非const修饰的成员函数,哪怕这个函数没有修改成员变量。
建议只要成员函数不修改成员变量就使用const修饰,例如show函数get等等。
8.2 const修饰对象
const修饰的对象被称为常量对象,这种对象的成员变量值无法修改,可以调用,也无法调用非const修饰的成员函数。
#include <iostream>
using namespace std;
class Demo
{
private:
int a;
public:
int b = 10;
Demo(int a)
{
this->a = a;
}
void func0()
{
cout << "普通成员函数" << endl;
}
int get_demo()const
{
return a;
}
void test() const // 常成员函数
{
// a++; // 错误const 修饰的成员函数,不能修改成员变量
cout << a << endl; // 可以调用,但是不能修改
// func0(); // 错误const修饰的成员函数,不能调用非const修饰的成员函数
get_demo();
}
};
int main()
{
const Demo demo(1);
// Demo const demo(1); // 两种初始化方式,等效于上一行。
cout << demo.get_demo() << endl;
// demo.func0(); // 错误 const修饰的对象,无法调用非const成员函数
demo.test();
cout << demo.b << endl;
// demo.b = 20; // 可以调用但是不能修改成员变量的值
return 0;
}
8.3 const修饰成员变量
const修饰的成员变量为常成员变量,表示该成员变量的值无法被修改。
常成员变量有两种初始化方式:
- 直接赋值 声明后赋值
- 构造初始化列表
同时使用时,前者失效,以后者为准。
#include <iostream>
using namespace std;
class Demo
{
private:
const int a = 1; // 直接赋值
const int b = 2;
const int c = 3;
public:
Demo(int a,int b,int c):a(a),b(b),c(c)
{
// this->a = 100; // const修饰的成员变量,值无法修改
}
void show()
{
cout << a << " " << b << " " << c << endl;
}
void test()
{
// a++; 错误const修饰的成员变量,可以调用但是不能修改。
// b++;
// c++;
}
};
int main()
{
Demo d(10,20,30);
d.show();
return 0;
}
8.4 const修饰局部变量
const修饰局部变量,表示该变量无法被修改。
这种方式常用于引用参数。
#include <iostream>
using namespace std;
class Demo
{
private:
const int a = 1; // 直接赋值
const int b = 2;
const int c = 3;
public:
Demo(int a,int b,int c):a(a),b(b),c(c)
{
// this->a = 100; // const修饰的成员变量,值无法修改
}
void show()
{
cout << a << " " << b << " " << c << endl;
}
void test()
{
int e = 1;
e = 10;
cout << e << endl;
const int f = 1;
// f = 10;
cout << f << endl;
}
};
int main()
{
Demo d(10,20,30);
d.show();
d.test();
return 0;
}