零基础C++入门详细教程
前言
你将学习到
| | | |
| ————————– | ———————– | ——————— |
| C++ 语言概述 | C++ 语言的输入输出 | 扩展名和注释符 |
| 基本数据类型 | 变量与常量 | 运算符和表达式 |
| 基本控制结构 | 函数定义与使用 | 类和对象的定义 |
| 数组的使用和初始化 | 指针的声明和使用 | 指针和引用的区别 |
一、C++语言
1. 实验介绍
C 语言是面向过程的程序设计语言,而 C++ 语言是由 C语言演变而来的一种面向对象的程序设计语言。本次实验共有两个内容,其一是简要介绍 C++ 语言的产生及其特点,其二是介绍 C++ 语言的基础支撑知识。
1.1 知识点
- C++ 语言的概述
- 扩展名和注释符
- 命名空间
- 标准 I/O 库
- 输入与输出
2. C++ 语言的概述
2.1 C++ 语言的产生
C 语言最初是贝尔实验室的 Dennis Ritchie 在 B 语言的基础上开发出来的。C语言具有运算符和数据结构丰富、语言简洁灵活、程序执行效率高等系列优点,被市场广泛应用。但 C 语言终究是面向过程的编程语言,与其他面向过程的编程语言一样,无法满足运用面向对象方法的开发需。而 C++ 语言便是在优化 C语言的基础上为支持面向对象的程序设计而研制的一个通用目的的程序设计语言。在后来的持续研究中,C++增加了许多新概念,例如虚函数、重载、继承、标准模板库、异常处理、命名空间等。
2.2 C++ 语言的特点
C++ 语言的特点主要表现在两个方面: 全面兼容 C 语言 和 支持面向对象的方法。
由于 C++ 可以全面兼容 C,这就使得部分由 C 语言编写的代码在未经修改的情况下就可以为 C++ 所用,这极大的促进了 C++ 的普及和面向对象技术的广泛应用。
C++ 最具有意义的一面就是支持面向对象的方法。与 C 兼容的特点虽赋予 C++ 双重特点,但 C 和 C++ 在概念上却是两种完全不同的程序设计语言,所以在 C++ 中应注意按照面向对象的思维去编写代码。
此外,C++ 在保持 C 简洁、高效等特点的基础上,对 C 的类型系统进行改革和扩充,因此 C++ 编译环境能检测出的类型错误更多。3. C++ 的扩展名和注释符
3.1 扩展名
C++ 源文件的扩展名与运行的具体编译器有关,一般有 .cc、.cxx、.cpp 和 .cp。与源文件相关的头文件扩展名一般是 .h。课程实验环境中的源文件扩展名一般为 .cpp,与之相关的头文件扩展名一般为 .h。
3.2 注释符
与其他语言类似,C++ 也有为代码提供辅助信息的注释符。
单行注释://注释内容int x; //定义 x 的数据类型为 int块注释:/ 注释内容 /
if(x>y)return x; else return y; /* 如果 x 大于 y 则返回 x 否则返回 y */注释符可以放置在代码的任何位置,且不参与代码运行。
4. 命名空间
命名空间里面包含了逻辑结构上相互关联的一组类、函数、模板等。命名空间像是一个容器,把某些在逻辑结构上相关的 “ 对象 ” 放在一起并与外界区分。特别的,命名空间里的变量名或类名可以和命名空间外的变量名或类名重名。
使用命名空间的时候,我们可以用 花括号 把声明块括起来,再以关键字 namespace 开头并命名,其基本格式如下:namespace 命名空间的名字 { 声明块 }其中声明块中可以包含变量、类、函数等。例如:
namespace S { int x; void FunA() { ... } }在命名空间外使用命名空间内的成员: 命名空间的名字加上作用域运算符 :: 。
参考上例 namespace S,若访问 x,可写成 S::x;访问 FunA() 则为 S::FunA()。
命名空间的嵌套, 具有分层屏蔽的作用。例如:namespace S1 { namespace S2 { int x; void FunA() { ... } } }若要访问 x,则可写为 S1::S2::x,同理,访问 FunA() 则可写为 S1::S2::FunA()。
using 声明:若命名空间内的某个成员使用了 using 声明,那么这个成员在程序的后续使用中,可直接使用成员名,无需添加限定修饰名。使用 using 声明: 以关键字 using 开头后跟命名空间内的成员名。参考上例嵌套的命名空间:
namespace S2 中 int x 的 using 声明为:using S1::S2::x; 。
注意: using namespace 后不能加类名或变量名。同上,若 namespace S2 中 int x 的 using 声明写为 using namespace S1::S2::x 便是错误的。
using 指示符可以一次性地使命名空间内的全体成员被直接使用。using 指示符的使用: 以关键字 using namespace 开头后跟命名空间名字。特别的,std 命名空间声明和定义了标准 C++ 库中的所有组件,使用 using namespace std; 则可使用标准 C++ 库中的所有成员。5. C++ 语言的输入与输出
C++ 标准 I/O 库包含 iostream、fstream 和 sstringstream。iostream、fstream 比较常用,一般操作于输入和输出,相较于前两者来说 sstringstream 的出现频率就低了许多,一般操作于数据的格式化。为了能更好的理解 C++ 语言的标准 I/O 库,我们参考 cplusplus 官网 的相关内容,整理注释了一份关于输入输出流类继承体系的关系图,可点击图片放大查看:
5.1 标准输出流对象 cout
预定义的插入符 “ << ” 作用在流类对象 cout 上可实现最基本的屏幕输出,其格式为:cout << 表达式1 << 表达式2...在输出语句中,若串联多个插入符,则可输出多个数据项。例如:
cout << a << b << c;则依次输出 a,b,c 三个数据项的值。
若插入符后面是复杂的表达式,则系统自动计算其表达式的值并传给插入符。例如:cout << "a+b=" << a+b依次输出字符串 “ a+b= ” 以及 a+b 的计算结果。
5.2 标准输入流对象 cin
预定义的提取符 “ >> ” 作用在流类对象 cin 上可实现对键盘输入的提取操作,其格式为:cin >> 表达式1 >> 表达式2...其中表达式通常用于存放输入值的变量。例如:
int a,b; cin >> a >> b;即编译执行后,输入 a 和 b 的值,a 和 b 之间使用空格分隔。如输入:1 2,则给 a 赋值为 1,b 赋值为 2。
5.3 简单的 I/O 格式控制
一般情况下,使用 cin 和 cout 进行数据的输入和输出时,会自动按照默认的格式进行处理。如若需要设定特殊的格式,可利用 C++ I/O 流类库提供的操纵符进行调整。这些操纵符可直接嵌入到输入输出语句中实现格式控制。常用的 I/O 流类库操纵符:
| 操纵符名 | 含义 |
| ———————————————— | —————————————- |
| dec | 数值数据采用十进制表示 |
| hex | 数值数据采用十六进制表示 |
| oct | 数值数据采用八进制表示 |
| ws | 提取空白符 |
| endl | 插入换行符,并刷新流 |
| ends | 插入空字符 |
| setsprecision(int) | 设置浮点的小数位数,包括小数点 |
| setw(int) | 设置域宽 |6. Hello shiyanlou
编写代码,使屏幕输出 `“Hello shiyanlou”`。
6.1 步骤
在实验楼环境 WebIDE 中新建一个文档,命名为 `shiyanlou.cpp`。
在代码文件编辑区输入实验代码:
#include <iostream> //iostream 表示对流运行读写操作。 using namespace std; //使用标准 C++ 库中的所有成员。 int main() { cout << "Hello shiyanlou!" << endl; //cout 为标准输出流对象;endl 表示插入换行符,并刷新流。 return 0; }完成代码编写并成功保存后,在 Linux 终端输入:
g++ shiyanlou.cpp -o shiyanlouGCC 和 G++ 是由 GNU 开发的编程语言编译器,其中 GCC 用来编译 C 语言程序;G++ 用来编译 C++ 语言程序。
g++ -o 的功能:生成可执行文件,其基本格式为:g++ 需生成可执行文件的源文件名 -o 可执行文件名称所以输入 `g++ shiyanlou.cpp -o shiyanlou` 命令且回车后,我们可以在代码文件浏览区看到生成的可执行文件 `shiyanlou`。
接着我们继续在 Linux 终端访问可执行文件:
./shiyanlou最终输出 `“Hello shiyanlou”“ 。
name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/shiyanlou.cpp error: 未创建 shiyanlou.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep cout /home/project/shiyanlou.cpp
error: 未输出 Hello shiyanlou 字符串
timeout: 2## 7. 实验总结 本次实验我们学习了以下几个知识点: - C++ 语言的概述
- 扩展名和注释符
- 命名空间
- 标准 I/O 库
- 输入与输出
本次实验在简要阐述 C++ 语言概要的基础上,介绍了 C++语言的扩展名、注释符、命名空间、输入输出等基础支撑知识,实践了经典范例 Hello shiyanlou。二、基本的数据类型及表达式
1. 实验介绍
数据是程序处理的对象,与其他高级语言类似,在 C++ 语言中不同类型数据的处理方式也是不同的。本次实验共有三个内容,一是介绍 C++ 语言的基本数据类型,二是介绍 C++ 中的变量与常量,三是介绍运算符和表达式。
1.1 知识点
- 基本数据类型
- string 类
- 结构体 struct
- 变量和常量
- 运算符和表达式
2. 基本数据类型
编写计算机程序的目的在于高效解决现实生活中的问题,正因如此,高级语言为我们提供了许多的数据类型。C++ 的数据类型分为基本数据类型和自定义数据类型,其基本数据类型如下:
| 数据类型名 | 长度(字节) | 取值范围 |
| ———————————————— | —————————————- | —————————————- |
| char | 1 | – 128 ~ 127 或 0 ~ 255 |
| bool | 1 | true, false |
| signed char | 1 | -128 ~ 127 |
| unsigned char | 1 | 0 ~ 255 |
| short | 2 | -32768 ~ 32767 |
| unsigned short | 2 | 0 ~ 65535 |
| int | 4 | -2147483648 到 2147483647 |
| unsigned int | 4 | 0 ~ 4294967295 |
| long | 8 | -9223372036854775808 到 9223372036854775807 |
| unsigned long | 8 | 0 到 18,446,744,073,709,551,615 |
| float | 4 | 3.4E-38 ~ 3.4E+38 |
| double | 8 | 1.7E-308 ~ 1.7E+308 |
| long double | 16 | 长双精度型 16 个字节(128 位)内存空间,可提供 18-19 位有效数字。 |各种类型的存储大小和具体操作系统的位数有关,目前大部分系统为 64 位,与上表基本一致。由上表可知,C++ 的基本数据类型一般包括:bool、char、int、float 和 double。其中关键字 signed、unsigned、short、long 被称为修饰符。特别的,当用 short 或 long 修饰 int 时,int 可省略不写,表示短整型数据或长整型数据。
2.1 string 类
string 是 C++ 标准库的一个重要部分,主要用于字符串的处理,string 类的头文件为:`#include
`。使用 string 可以对字符串的对象执行某些操作,例如拼接、查找等。
此课程属性为基础入门,所以暂不对 string 类进行过多介绍。若想要深度了解或学习 string 类的相关知识,可在本次实验后继续学习 C++ 进阶应用实战 – 初识类与对象。2.2 struct
struct 是由一系列相同或不同的数据类型组成的数据集合,也称结构体,是一种复合型的数据类型。
C++ 语言中 struct 的基本结构struct struct_name { memberList }varList其中 `struct` 为结构体的关键字,`struct_name` 为结构体的名字,`memberList` 为结构体成员变量及成员函数的列表,`varList` 为结构体声明的变量。使用`varList`元素名 可调用结构体中的元素。
例如在实验环境中新建一个名为 `stru.cpp` 的文件并输入:#include <iostream> using namespace std; struct shiyanlou //定义 shiyanlou 结构体。 { int a; //定义 a 的数据类型为整型。 int pri() {return a;}//定义 pri() 的功能为返回 a 的值。 }s1; int main() { cout<<"sbefore:"<<s1.pri()<<endl; s1.a = 6; cout<<"safter:"<<s1.pri()<<endl; return 0; }其中 `sbefore` 输出未初始化变量的结果,`safter` 输出已初始化变量的结果:
name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/stru.cpp error: 未创建 stru.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep struct /home/project/stru.cpp
error: 未声明结构体
timeout: 2C++ 语言中 struct 与 C 语言中 struct 的区别
- 在 C++ 语言中,可直接使用结构体名来声明变量;在 C 语言中,不能直接使用结构体名来声明变量,需前缀 struct。
- C++ 语言的 struct 中既可以定义成员变量又可以定义成员函数;C 语言的 struct 中只能定义成员变量,不能定义成员函数。例如:
struct shiyanlou { int a; int pri() {return a;} }s1;在 C++ 的编译过程中,此结构体不会出错。但若在 C 语言中,由于 int pri() {return a;} 为成员函数,会出现编译报错。
3. 变量
变量来源于数学,是计算机语言中能储存计算结果且在程序执行过程中可以变化的量。变量需要用名字来标识且需指定相应的数据类型,变量声明的形式如下:
数据类型 变量名1,变量名2...注意:变量的命名不是随意的,需遵守以下规则:
- 变量名只能包含字母(A-Z,a-z)和数字(0-9)或者下划线(_)。
- 变量名首位必须是字母或下划线。
- 不能使用 C++ 的关键字来命名变量,以免冲突。例如:struct。
- 变量名区分大小写。
-
例如声明两个 int 类型的变量:
int a_1,b变量分为局部变量与全局变量。由某对象或某个函数所创建的变量通常都是局部变量,只能被内部引用,而无法被其它对象或函数引用,全局变量是可以被本程序所有对象或函数引用。
3.1 局部变量
局部变量 只作用于当前模块。例如新建一个名为 `localvar.cpp` 的文件并输入:
#include <iostream> using namespace std; int main() { int a = 2; //此局部变量只作用于当前模块。 cout<<a<<endl; return 0; }在主函数 `int main()` 中定义了一个局部变量 a 且赋值为 2,若在主函数中对局部变量 a 进行输出,则直接输出 2。
程序结果:3.2 全局变量
当两个或两个以上模块或函数需要用到同一个变量时,可以把变量定义为 全局变量。例如新建一个名为 `globalvar.cpp`的文件并输入:
#include <iostream> using namespace std; int global1 = 1; //定义全局变量 global1。 void func() { cout<<global1<<endl; //输出全局变量 global1 的值。 } int main() { cout<<global1<<endl; //输出全局变量 global1 的值。 func(); //调用函数 func()。函数的调用会在后面的实验详细讲解,在此只做了解。 return 0; }上述代码中,`void func()` 和 `int main()`都需要使用变量 global1,即定义其为全局变量,供多个模块使用。
程序结果:3.3 重名现象
当全局变量与局部变量重名的时候,起作用的是局部变量,全局变量被屏蔽掉。我们修改 globalvar.cpp文件为以下内容:
#include <iostream> using namespace std; int global1 = 1; //定义全局变量 global1。 void func() { cout<<global1<<endl; //输出全局变量 global1 的值。 } int main() { int global1 = 10; //定义局部变量 global1 屏蔽全局变量 global1。 cout<<global1<<endl; //输出局部变量 global1 的值。 func(); //调用函数 func()。函数的调用会在后面的实验详细讲解,在此只做了解。 return 0; }我们在 `int main()` 中定义局部变量 `int global1`,此时出现重名现象。则 `int main()` 中 `cout<
程序结果: name: 检查目录是否创建1 script: | #!/bin/bash ls /home/project/localvar.cpp error: 未创建 localvar.cpp 文件 timeout: 1
- name: 检查目录是否创建2
script: |!/bin/bash
ls /home/project/globalvar.cpp
error: 未创建 globalvar.cpp 文件
timeout: 2 - name: 检查文件内容2
script: |!/bin/bash
grep global1 /home/project/globalvar.cpp
error: 未定义全局变量
timeout: 3## 4. 常量 常量是指具有固定值的表达式,其值在程序运行的整个过程中是不可改变的。常量一般分为整型常量、实型常量、字符常量、字符串常量和布尔常量。 ### 4.1 整型常量 即以文字形式出现的整数,包括正整数、负整数和零。在 C++ 中支持十进制、八进制和十六进制的整数。 十进制表示的整型常量:由 - / + 和 若干 0 - 9 的数字组合,例如:2、- 2、356 等。 八进制表示的整型常量:以 0 开头后跟若干个 0 - 7 数字,例如:0333、06 等。 十六进制表示的整型常量:以 0x 开头后跟 0 - 9 的数字及 A-F 的字母,例如:0x12、0x3A 等。 一般的,当十进制表示的整型常量为正整数时,可省略 +;八进制和十六进制通常表示无符号的整数,所以不必添加 - / +。 ### 4.2 实型常量 即以文字形式出现的实数,包括一般形式和指数形式。 一般形式,即正常表达的数字,例如:3.14、- 6.78 等。 指数形式,通俗来说就是我们在数学中常用的 n 乘以 10 的 x 次方,例如:3.1E+6 表示的则是 3.1 乘以 10 的 6 次方,即 3100000;6.9E-2 表示的则是 6.9 乘以 10 的 - 2 次方,即 0.069。 ### 4.3 字符常量 用单引号括起来的一个字符称为字符常量。例如:'a'、'D' 等。 例如回车、换行、制表符等既无法显示又不能输入的特殊字符,需要利用 C++ 预定义的转义序列在程序中表示。下面是常用的预定义的转义序列表: | 字符常量形式 | 含义 | | ----------------------------------------- | -------------------------------------- | | n | 换行 | | r | 回车 | | t | 水平制表符 | | v | 垂直制表符 | | a | 响铃 | | f | 换页 | | | 字符 ' ' | | " | 双引号 | | ' | 单引号 | 例如:`input:n` 则在输出字符串 `'input:'`后执行换行操作。 ### 4.4 字符串常量 用双引号括起来的字符序列称为字符串常量。例如:"shiyanlou"、"an apple" 等。 若要在字符序列中包含双引号,例如表示语句:You choose "time" or "happy",我们则需利用预定义的转义序列 `"`来表示句中的双引号。定义其字符串常量为:`"You choose "time" or "happy""`。 ### 4.5 布尔常量 布尔常量只有 TRUE 和 FALSE 两种。 ### 4.6 常量声明 常量声明一般有两种方式,一是使用 #define 预处理、二是使用 const 关键字。
- 使用 #define 预处理
格式:`#define name value`#define PI 3.14159 #define NEWLINE 'n' - 使用 const 关键字
格式:`const type name = value`const double pi = 3.14; const char tab = 't';5. 运算符与表达式
表达式是运算符及其运算数的序列,它指定一项计算。一般分为算数运算符和算数表达式、逻辑运算符和逻辑表达式、关系运算符和关系表达式、条件运算符和条件表达式。
5.1 算数运算符和算数表达式
算数运算符包括基本算数运算符和自增自减运算符;算数表达式一般由算数运算符、操作数和括号组成。下面是基本的算数运算符和算数表达式:
| 运算符名 | 语法 | 描述 | 表达式 |
| ———— | —————– |—————– |————– |
| 一元加 | +a | 正号 | +a=6 |
| 一元减 | -a | 负号 | -a=-6 |
| 加 | a + b | 两数相加 | a + b = 6 |
| 减 | a – b | 两数相减 | a – b = 6 |
| 乘 | a b | 两数相乘 | a b = 8 |
| 除 | a / b | 两数相除 | a / b = 8 |
| 模 | a % b | 两数整除后取余 | a % b = 3 |
| 自增 | a++ 或 ++a | 整数本身加一 | a++ = 6 或 ++a = 6 |
| 自减 | a– 或 –a | 整数本身减一 | a– = 6 或 –a = 6 |
a++ 和 ++a 的区别:++a 称为前缀形式,在表达式计算之前完成自加运算;a++ 称为后缀形式,在表达式计算之后完成自加运算。同理,–a 和 a– 亦如此,–a 在表达式计算之前完成自减运算;a– 在表达式计算之后完成自减运算。例如新建 `self.cpp` 文档,输入:#include <iostream> using namespace std; int main() { int i=1; cout<<++i<<endl; //输出的 i 值为 2。 cout<<i++<<endl; //输出的 i 值为 2。 cout<<i<<endl;//输出的 i 值为 3。 }其中 `cout<<++i<
5.2 逻辑运算符和逻辑表达式
逻辑表达式是通过逻辑运算符将操作数或等式进行逻辑判断的语句。在下面基本的逻辑运算符和逻辑表达式中,我们假设布尔变量 a 为 TRUE,b 为 FALSE:
| 运算符 | 运算符名 | 描述 | 表达式及结果 |
| ———— | —————– |—————– |————– |
| && | 与 | 当且仅当所有操作数都为 TRUE,表达式才为 TRUE | (a && b) 为 FALSE|
| 11 | 或 | 任一操作数为 TRUE,表达式为 TRUE | (a 11 b) 为 TRUE |
| ! | 非 | 取反。若操作数为 TRUE,表达式为 FALSE;若操作数为 FALSE,表达式为 TRUE | (!b) 为 TRUE |5.3 关系运算符和关系表达式
关系表达式最终产生的是一个布尔结果,它使用关系运算符判断操作数值之间的关系。若关系正确,则结果为 TRUE,若关系错误,则结果为 FALSE。在下面基本的关系运算符和关系表达式中,我们假设 a 为 3,b 为 4:
5.4 条件运算符和条件表达式
在 C++ 中条件运算符 ? 是三元运算符,其表达式为:
条件表达式 ? 表达式1 : 表达式2如果条件表达式的值为 `TRUE` ,则返回 `表达式1` 的值,否则返回 `表达式2` 的值。
例如新建 condition.cpp 文件,输入:#include <iostream> using namespace std; int main() { int a = 5; int b = 3; int c; c=(a>b)? a : b; //如果a大于b则返回a,否则返回b cout<<c<<endl; }c 用于接收三元运算符的返回值,又因为 a>b 的布尔值为 TRUE,所以返回 a 的值。
程序结果:5.5 拓展
- sizeof 运算符 用于计算 对象 类型或 表达式结果类型在内存中所占的字节数,其形式为:`sizeof (对象类型名)` 或 `sizeof(表达式)`。
- 在 C++ 中逗号也是一个运算符,称为 逗号运算符,其形式为:`表达式1,表达式2`。定义了从左到右的求解顺序,例如 `a = 1,a + 1` 其值为 2。
- C++ 中最简单的赋值运算符是 ‘=’。例如 `a = (x=4) / (y=2)` 表达的则是 x 赋值为 4,y 赋值为 2,a 赋值为 4 / 2,即 2。除了 `=`,C++ 中还有一些其他比较常用的赋值运算符:
6. 实验总结
本次实验学习了以下几个知识点:
- 基本数据类型
- string 类
- 结构体 struct
- 变量和常量
- 运算符和表达式
此次实验我们了解了 C++ 基本的数据类型,对比了 struct 在 C 和 C++ 中的不同之处,掌握了常量和变量的声明方式,细解了各种类型的运算符和表达式,实夯 C++ 语言之基。三、内存分配
1. 实验介绍
C++ 程序在运行时,计算机内存被分为程序代码区、全局数据区、堆区、栈区四个部分。本次实验共有两个内容,一是介绍 C++ 三种内存分配方式,二是使用 new 关键字和 delete 关键字管理堆内存。
1.1 知识点 - 静态存储分配
- 栈内存分配
- 堆内存分配
- new 关键字和 delete 关键字
2. 内存分配的三种方式
不同的内存分配方式,有不同的内存调度机制。C++ 语言的三种内存分配方式分别是:静态存储区分配、栈内存分配和堆内存分配。
2.1 静态存储区分配
静态分配方式下的内存在程序编译的时候就已经分配好了且存在于程序的整个运行过程。例如 static 变量,其生命周期随程序的结束而结束,而非像一般变量随函数或作用域的结束而结束。
除了 static 变量,还有一种全局对象 static object,也可以存放在静态存储区。2.2 栈内存分配
栈内存分配方式下的内存是存在于某作用域的一块空间。例如调用某个函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束时,这些内存单元会被自动释放。
函数内局部变量的生命周期:起于变量声明,止于函数执行结束。2.3 堆内存分配
堆内存分配,也称 动态内存分配,通常使用 new 和 delete 关键字进行管理。堆内存空间可由用户手动分配和释放,所以其生存周期由用户指定,较为灵活。但频繁的分配、释放大小不同的堆空间会产生内存碎片。
3. 管理堆内存
C 语言中一般使用 malloc() 函数来分配内存空间,free() 函数来释放内存空间。但在 c++ 语言中我们使用 new 关键字来分配内存空间,delete 关键字来释放内存空间。后者在性能等方面优于前者。
在学习使用 new 和 delete 关键字之前我们先初步了解一下指针的概念及其一般形式。
指针是一个变量,其值是另一个变量的地址。指针变量声明的一般形式为:type *pointerVar-name其中 `type` 为指针的数据类型,`*` 是定义指针的关键符,`pointerVar-name` 指定指针变量名。
例如:`char *p`,即定义了一个字符型指针变量 p。
指针的另一些基本内容会在后续的实验中详细介绍,本次实验对指针的内容仅做了解。现在我们正式学习 new 关键字和 delete 关键字。3.1 new 关键字
new 关键字 用于堆内存的分配,可自动计算所要分配内存空间的大小,其基本使用形式为:
指针变量名 = new 类型例如:
int *p; //定义一个整型指针变量 p = new int;其中 `p = new int`; 表示动态分配一个存放整型数据的内存空间,并将其首地址赋给整型指针变量 p,此时省略指针变量 p 前的 `*`。
delete 关键字 用于堆内存的释放,其基本形式为:
`C++ delete 指针变量 `
例如:int *p; //定义一个整型指针变量 p = new int; //省略指针操作过程 delete p;其中 `delete p`; 表示释放指针变量 p 指向的内存空间,此时省略指针变量 p 前的 `*`。
注意:使用 new 获取的内存空间必须使用 delete 进行释放。
我们用实例来加深一下 new 和 delete 关键字的使用方式,新建 `mans.cpp` 文件,输入:#include <iostream> using namespace std; int main() { int *p; //定义一个整型的指针变量 p。 p = new int; //动态分配一个存放整型数据的内存空间,并将其首地址赋给整型指针变量 p。 *p = 6; //为指针指向的内存块赋值为 6。 cout<<*p<<endl; //输出内存块的赋值。 delete p;//释放指针变量 p 指向的内存空间。 return 0; }程序首先定义了整型指针变量 p,然后使用 new 关键字为其分配 int 型的内存空间,并让指针 p 指向分配的内存空间。随后为内存块赋值为 6,并且输出所赋的值。最后释放指针变量 p。
程序结果为:name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/mans.cpp error: 未创建 mans.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep delete /home/project/mans.cpp
error: 内存分配有误
timeout: 2## 4. 实验总结 本次实验学习了以下几个知识点: - 静态存储分配
- 栈内存分配
- 堆内存分配
- new 关键字和 delete 关键字
此次实验我们在了解 C++ 三种内存分配方式的基础上,进一步学习了其中堆内存的管理,即动态内存的分配和释放,掌握了 new 和 delete 关键字的一般形式和操作方式。四、基本控制结构
1. 实验介绍
C++ 语言的基本控制结构包括顺序结构、选择结构和循环结构三种。前面实验涉及到的控制结构均为顺序结构,即程序是顺序执行的语句序列。但若想解决稍复杂的问题,例如求解分段函数、输入一个班级的学生成绩并计算其平均值等,则需要利用选择结构或循环结构。
1.1 知识点
- 顺序结构
- 选择结构
- 循环结构
- 循环嵌套和死循环
- break、continue 以及 goto 语句的使用
2. 顺序结构
顺序结构是最简单的控制结构,也是 C++ 程序默认的执行流程。在此结构中,程序将按照书写的先后顺序自上而下依次执行。一般情况下,若程序中不包含选择结构、循环结构和跳转语句,则此程序为顺序结构。例如新建 `sequence.cpp` 文件,输入:
#include<iostream> using namespace std; int main() { int a,b; a=3; b=5; cout<<a+b<<endl; //输出 a+b 的值。 return 0; }此程序中,语句按照从上至下的顺序依次执行:首先定义了 a 和 b 两个 int 型变量,然后给 a 赋值为 3,b 赋值为 5,最后输出 a+b 的结果。
程序结果:3. 选择结构
根据判定条件的结果,选择相应执行语句的控制结构称为选择结构。
3.1 使用条件运算符实现选择结构
利用条件运算符 ? 可以实现一个简单的选择结构,其基本形式为:
条件表达式 ? 表达式1 : 表达式2此方式需根据条件表达式的判断结果,选择表达式 1 或者表达式 2。其具体意义是:若条件表达式的布尔值为 `TRUE` ,则返回 `表达式1` 的值,否则返回 `表达式2` 的值。例如新建 `choose1.cpp` 文件,输入:
#include<iostream> using namespace std; int main () { int x,y=7; //声明局部变量 x 和 y 并给 y 赋值为 7。 x=(y<6) ? 1 : 0; //使用条件运算符实现选择结构 cout<<"choose:"<<x<<endl; //输出返回结果 return 0; }程序中 `x=(y<6) ? 1 : 0;` 表达的是:若 y 小于 6 的布尔值为 TRUE,则返回 1 给 x,否则返回 0 给 x。显然,y 的值为 7,7 < 6 的值为 FALSE,即返回 0。
程序结果:3.2 使用 if 语句实现选择结构
if 是典型的用于实现选择结构的语句,例如 `if(X==1) i++;` 当满足条件 `X==1` 时,执行语句 `i++`。if 语句一般分为简单的 if…else 语句、嵌套的 if 语句和 if…else if 语句三类。
简单的 if…else 语句 的基本结构为:if(判定条件){ 判定条件为 TRUE 时的执行语句 } else{ 判定条件为 FALSE 时的执行语句 }和使用条件运算符实现选择结构的方式类似。首先进入判定条件,若判定条件的布尔值为 TRUE 则执行 if 花括号内的语句,不执行 else 花括号内的语句;若判定条件的布尔值为 FALSE 则执行 else 花括号内的语句,不执行 if 花括号内的语句。例如新建 `choose2.cpp` 文件,输入:
#include<iostream> using namespace std; int main () { int x,y=7; //声明局部变量 x 和 y 并给 y 赋值为 7。 if(y<6){ //判定 y 是否小于 6。 x=1; //判定条件为 TRUE 时,x = 1。 } else{ x=0; //判定条件为 FALSE 时,x = 0。 } cout<<"choose:"<<x<<endl; //输出 x 的值。 return 0; }因为 y = 7 即 y < 6 的布尔值为 FALSE,所以 x 最后输出的值为 0。
程序结果:嵌套的 if 语句 的基本结构为:
if(判定条件 1){ if(判定条件 2){ 判定条件 1 和判定条件 2 均为 TRUE 时的执行语句 } else{ 判定条件 1 为 TRUE 且判定条件 2 为 FALSE 时的执行语句 } } else{ if(判定条件 3){ 判定条件 1 为 FALSE 且判定条件 3 为 TRUE 时的执行语句 } else{ 判定条件 1 和判定条件 3 均为 FALSE 时的执行语句 } }嵌套的 if 语句只是把简单的 if…else 语句中的执行语句换成了 if…else 语句。当判定条件 1 的布尔值为 TRUE 时,进入判定条件 2;当判定条件 1 的布尔值为 FALSE 时,进入判定条件 3。例如新建` choose3.cpp `文件,输入:
#include<iostream> using namespace std; int main () { int x;//声明局部变量 x。 int y=7; //声明局部变量 y 且赋值为 7。 int z=3;//声明局部变量 z 且赋值为 3。 if(y<6){ //判定 y 是否小于 6。 x=1; //判定条件 y<6 为 TRUE 时,x = 1。 } else{ //判定条件 y<6 为 FALSE 时。 if(z>2){ // 判定 z 是否大于 2。 x=2; //判定条件 z>2 为 TRUE 时。 } else{ x=3; //判定条件 z>2 为 FALSE 时。 } } cout<<"choose:"<<x<<endl; //输出 x 的值。 return 0; }因为 y = 7 即 y < 6 的布尔值为 FALSE,所以进入 else 分支继续判定 z 是否大于 2,又因为 z=3 即 z>2 的布尔值为 TRUE,所以 x 输出 2。
程序结果:if…else if 语句 的基本结构为:
if(判定条件1){ 满足判定条件 1 时执行的语句 } else if(判定条件2){ 满足判定条件 2 时执行的语句 } else{ 既不满足判定条件 1,也不满足判定条件 2 时的执行语句 }注意:嵌套 if 语句的判定条件可针对不同对象进行判定,例如上例,先判定条件 `y<6`,然后在` y<6 `的布尔值为` FALSE `的情况下嵌套另一个针对不同对象的判定条件` z>2`。而 if…else if 语句只能针对同一对象进行判定,例如新建` choose4.cpp `文件,输入:
#include<iostream> using namespace std; int main () { int x,y=7; //声明局部变量 x 和 y 并给 y 赋值为 7。 if(y>8){ //判定 y 是否大于 8。 x=1; //当 y>8 时 x = 1。 } else if(y>5){ x=2; //当 y>5 时 x = 2。 } else{ x=3; //若既不满足 y>8,也不满足 y>5 时,x = 3。 } cout<<"choose:"<<x<<endl; //输出 x 的值。 return 0; }因为 y=7 不满足 y>8 而满足 y>5,所以 x 的值为 2。
程序结果:name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/choose2.cpp error: 未创建 choose2.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep if /home/project/choose2.cpp
error: 未使用 if 语句实现选择结构
timeout: 2 - name: 检查目录是否创建2
script: |!/bin/bash
ls /home/project/choose3.cpp
error: 未创建 choose3.cpp 文件
timeout: 3 - name: 检查文件内容2
script: |!/bin/bash
grep if /home/project/choose3.cpp
error: 未使用 if 语句实现选择结构
timeout: 4 - name: 检查目录是否创建3
script: |!/bin/bash
ls /home/project/choose4.cpp
error: 未创建 choose4.cpp 文件
timeout: 5 - name: 检查文件内容3
script: |!/bin/bash
grep if /home/project/choose4.cpp
error: 未使用 if 语句实现选择结构
timeout: 6### 3.3 使用 switch 语句实现选择结构 switch 语句比较特殊,其目的是检查常量表达式中的值,其基本结构为: ```C++ switch(表达式){ case 值1: 代码块1 break; case 值2: 代码块2 break; ... default: 默认执行的代码块 }当 switch 表达式的值和 case 语句选择的值相同时,从此 case 位置开始向下执行,直到遇到 break 语句或 switch 语句块结束;如果没有匹配的 case 语句则执行 default 块的默认代码。例如新建` choose5.cpp `文件,输入:
#include<iostream> using namespace std; int main () { cout<<"输入(a/s/d/w)"<<endl; //输出提示信息。 char ch; //定义 char 型变量 ch。 cin>>ch; //从键盘输入 ch 的值。 switch(ch){ case 'w': //键入 w 时。 cout<<"上"<<endl; break; case 's': //键入 s 时。 cout<<"下"<<endl; break; case 'a': //键入 a 时。 cout<<"左"<<endl; break; case 'd': //键入 d 时。 cout<<"右"<<endl; break; default: //键入值不是 a/s/d/w 时。 cout<<"输入错误!"<<endl; } return 0; }键入 w 输出文本 “上” 并跳出 switch 语句;键入 s 输出文本 “下” 并跳出 switch 语句;键入 a 输出文本 “左” 并跳出 switch 语句;键入 d 输出文本 “右” 并跳出 switch 语句;键入非 a/s/d/w 中的字符,则输出提示信息 “输入错误!”。
键入 ‘a’ 的程序结果:键入 ‘c’ 的程序结果:
name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/choose5.cpp error: 未创建 choose5.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep switch /home/project/choose5.cpp
error: 未使用 switch 语句实现选择结构
timeout: 2## 4. 循环结构 允许多次执行某一条语句或某个语句组的控制结构称为循环语句,在 C++ 中有 while 语句、do...while 语句、for 语句三种循环控制语句。 ### 4.1 使用 while 语句实现循环结构 while 语句的执行过程是先判断,再执行。其基本结构为: ```C++ while(条件){ 代码块 }下面是 while 语句的流程图:
执行 while 语句时,首先判断 while 后面的条件是否成立( TRUE or FALSE )。若成立,则执行循环内的代码,直到条件不成立;若不成立,则执行 while 语句后的其他语句。例如新建 circle1.cpp 文件,输入:
#include<iostream> using namespace std; int main() { int sub = 0; //定义变量 sub 并赋值为 0。 int i = 1; //定义变量 i 并赋值为 1。 while(i<5){ //判定 i<5 是否成立,若成立则执行循环体内的语句,若不成立则执行 while 语句后的其他语句。 sub += i; //执行 sub = sub + i。 i++; //i 累加 1 } cout<<sub<<endl; //输出 sub 的值。 return 0; }在此程序中,sub 的初始值为 0,i 的初始值为 1。执行到 while 语句时,i<5 成立,进入循环体执行。
第一次循环:sub=0+1=1;执行 i++ 语句后的 i 值为 2,i<5 成立,继续执行循环体内的语句。
第二次循环:sub=0+1+2=3;执行 i++ 语句后的 i 值为 3,i<5 成立,继续执行循环体内的语句。
第三次循环:sub=0+1+2+3=6;执行 i++ 语句后的 i 值为 4,i<5 成立,继续执行循环体内的语句。
第四次循环:sub=0+1+2+3+4=10;执行 i++ 语句后的 i 值为 5,i<5 不成立,执行 while 语句后的其他语句。
最后输出 sub 的值为 10。
程序结果:4.2 使用 `do…while` 语句实现循环结构
`do…while` 语句的执行过程是先执行,再判断。其基本结构为:
do{ 代码块 }while(条件);下面是 `do…while` 语句的流程图:
执行 do…while 语句时,先执行一次循环体内的语句,然后再判断循环条件是否成立。如果条件成立,则继续执行,直到循环条件不成立;若条件不成立,执行 while 语句后的其他语句。例如新建 circle2.cpp 文件,输入:
#include<iostream> using namespace std; int main() { int sub = 0; //定义变量 sub 并赋值为 0。 int i = 1; //定义变量 i 并赋值为 1。 do{ sub += i; //执行 sub = sub + i。 i++; //i 累加 1 }while(i<5);//判定 i<5 是否成立,若成立则执行循环体内的语句,若不成立则执行 do...while 语句后的其他语句。 cout<<sub<<endl; //输出 sub 的值。 return 0; }在此程序中,sub 的初始值为 0,i 的初始值为 1。执行到 do…while 语句时,直接进入循环体执行。
第一次循环:sub=0+1=1;执行 i++ 语句后的 i 值为 2。
i<5 成立,执行第二次循环:sub=0+1+2=3;执行 i++ 语句后的 i 值为 3。
i<5 成立,执行第三次循环:sub=0+1+2+3=6;执行 i++ 语句后的 i 值为 4。
i<5 成立,执行第四次循环:sub=0+1+2+3+4=10;执行 i++ 语句后的 i 值为 5。
i<5 不成立,执行 do…while 语句后的其他语句。
最后输出 sub 的值为 10。
程序结果:4.3 使用 for 语句实现循环结构
for 语句允许指定循环的增量。其基本结构为:
for(循环变量初始化;循环条件;循环增量) { 代码块 }下面是 for 语句的流程图:
使用 for 语句实现循环结构的第一步就是初始化循环变量,然后判定循环条件,如果布尔值为 TRUE 则执行代码块中的语句,直到条件判定的布尔值为 FALSE 时终止循环;否则,循环结束。例如新建 circle3.cpp文件,输入:
#include<iostream> using namespace std; int main() { int sub = 0; //定义变量 sub 并赋值为 0。 int i; //定义变量 i。 for(i=1;i<5;i++) //设定 i 的初始值为 1,循环条件为 i<5,循环增量为 i++。 { sub += i; //执行 sub = sub + i。 } cout<<sub<<endl; //输出 sub 的值。 return 0; }执行 for 语句。设定 i 的初始值为 1,判定条件为 i<5,循环增量为 i++。
i<5 成立,执行第一次循环:sub=0+1=1;返回到循环增量处,执行 i++ 语句,此时 i=2,i<5 成立。
执行第二次循环:sub=0+1+2=3;返回到循环增量处,执行 i++ 语句,此时 i=3,i<5 成立。
执行第三次循环:sub=0+1+2+3=6;返回到循环增量处,执行 i++ 语句,此时 i=4,i<5 成立。
执行第四次循环:sub=0+1+2+3+4=10;返回到循环增量处,执行 i++ 语句,此时 i=5,i<5 不成立,退出 for 循环。
最后输出 sub 的值为 10。
程序结果:name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/circle3.cpp error: 未创建 circle3.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep for /home/project/circle3.cpp
error: 未使用 for 语句实现循环结构
timeout: 2### 4.4 拓展
- 与 if 嵌套语句类似,循环结构也可以进行 循环嵌套。循环结构为:
for 嵌套for(循环变量初始化1; 循环条件1; 循环增量1) { for(循环变量初始化2; 循环条件2; 循环增量2){ 代码块2 } 代码块1 }while 嵌套
while(条件1){ while(条件2){ 代码块2 } 代码块1 }do-while 循环
do{ 代码块1 do{ 代码块2 }while(条件2); }while(条件1);注意:尽量避免设计层次过多的循环嵌套。
- 循环结构和选择结构可以互相嵌套,例如在 for 语句中嵌套 if 语句:
for(循环变量初始化;循环条件;循环增量) { ... if(判定条件){ 判定条件为 TRUE 时的执行语句 } else{ 判定条件为 FALSE 时的执行语句 } } - 判定条件永恒为 TRUE 的循环称为 死循环。例如:
int i=1 while(i==1){ cout<<"1"<<endl; }由于判定条件` i==1 `的布尔值永恒为 TRUE,所以程序将无限循环输出字符串 “1”。值得注意的是,程序中应不含死循环,除非有特殊需求。
5. 其他控制语句
5.1 ` continue ` 与` break `语句的区别
continue:结束本次循环,所在循环体并未结束。例如新建` other.cpp `文件,输入:
#include<iostream> using namespace std; int main(){ int i; for (i=0;i<5;i++) { if (i==3) continue; cout<<i<<endl; } return 0; }循环至` i=3 `时,满足条件` i==3 `执行` continue `语句:直接跳出 本次循环 且不执行循环体内 ` continue `后的其他语句。随后再次执行循环增量` i++ `语句,判定循环条件,执行循环体。
程序结果:break:结束整个循环体,所在循环体已结束。将 other.cpp 文件中的 continue 修改为 break,如下:
#include<iostream> using namespace std; int main(){ int i; for (i=0;i<5;i++) { if (i==3) break; cout<<i<<endl; } return 0; }循环至` i=3 `时,满足条件` i==3 `执行` break `语句,直接退出` for 循环`,结果只输出数字 3 前面的数字 0 1 2。
程序结果:shiyanlou:project/ $ g++ other.cpp -o other shiyanlou:project/ $ ./other 0 1 25.2 goto 语句的使用
goto 语句允许把控制无条件转移到同一函数内的标记语句。但在任何的编程语言中,都不建议使用 goto 语句,因为它使得程序的控制流难以跟踪。
goto 语句的语法:goto label; ... label: statement;当执行` goto label; `语句时,直接跳转并执行` label `标记的语句。例如新建` gtacc.cpp `文件,输入:
#include<iostream> using namespace std; int main(){ int i=3; if (i==3){ goto acc; } else{ cout<<i<<endl; } acc:cout<<"acc"<<endl; //执行 goto acc 时跳转至本条语句。 return 0; }因为` if `语句中判定条件` i==3 `的布尔值`为 TRUE `,所以执行语句` goto acc`; 后直接跳转到语句` cout<<"acc"<
程序结果: 6. 实验总结
本次实验学习了以下几个知识点:
- 顺序结构
- 选择结构
- 循环结构
- 循环嵌套和死循环
- break、continue 以及 goto 语句的使用
此次实验我们着重学习了 C++顺序结构、选择结构和循环结构的基本形式和使用方式,了解了死循环和循环嵌套的概念,拓展介绍了 break 语句、continue 语句及 goto 语句的使用方式。五、函数的定义和使用
1. 实验介绍
在 C++ 中,函数是一组给定名称的语句。每个 C++ 程序都必须有一个主函数 main(),它是程序执行的起点。一个 C++ 程序由一个主函数和若干子函数组成。本次实验共有三个内容:函数的定义与调用、参数传递及拓展知识。
1.1 知识点
- 函数的定义
- 函数的声明与调用
- 参数传递
- 默认参数
- 函数重载
2. 函数的定义
C++ 中的函数分为 C++ 标准库函数和自定义函数。使用标准库函数需要包含特定的头文件,比如使用 vector 功能时,需包含头文件 #include
。而自定义函数是用户自定义命名和声明的一组语句,其基本结构为: type name(parameter list){ code }其中 type 为函数的返回值类型;name 为函数的名称,也是调用函数所使用的标识符;parameter list为传递给函数的参数列表;code 为函数的主体,存放函数需要执行的语句。例如我们定义一个 add 函数:
int add(int a,int b){ return a + b; }add 函数的功能是利用 return 关键字返回 a 加 b 的和。
3. 函数的声明与调用
3.1 函数的声明
如果希望在定义函数前调用函数,则需要在调用函数前声明函数,其结构为:
type name(parameter list);其中 type 为函数的返回值类型;name 为函数的名称;parameter list 为传递给函数的参数列表。例如上例的 add 函数,如果要在定义 add 函数前调用 add 函数,则需要在调用此函数前,对函数进行声明:
int add(int a,int b); //函数声明。 int main(){ 调用 add 函数 //在未定义函数前调用函数。 } int add(int a,int b){ return a + b; } //定义函数3.2 函数的调用
调用函数时,需暂停主调函数的执行,转入执行被调函数,当被调函数执行至 return 语句或函数末尾时,被调函数执行完毕,转回执行主调函数。可按如下格式调用函数:
函数名(实参列表)注意:实参列表应与函数原型的参数个数及参数类型一致。
新建` addhs.cpp `文件,输入:#include <iostream> using namespace std; int add(int a,int b); //函数声明。 int main() { int x,y; cout<<"input x y:"; //输出提示信息。 cin>>x>>y; //通过输入的方式给 x y 赋值。 int c=add(x,y);//在未定义函数前调用函数。将函数的返回结果赋给变量 c。 cout<<c<<endl;//输出 c 值。 return 0; } int add(int a,int b) { return a + b; }//定义函数执行语句` int c=add(a,b) `时,暂停执行` int main() `函数,转入执行` add `函数。把用户输入的` x、y `值传给` add `函数的变量` a、b `,最后将语句` return a + b; `的结果赋给变量 c。
输入 x 值 和 y 值时,中间使用空格分隔,程序结果如下:name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/addhs.cpp error: 未创建 addhs.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep add /home/project/addhs.cpp
error: 未定义 add 函数
timeout: 2## 4. 参数传递 在定义函数时,参数是没有实际值的,只起一个占位作用,所以称为形式参数,简称 "形参";在调用函数时,参数必须有一个确定的值,是真正能对结果起作用的因素,所以称为实际参数,简称 "实参"。函数的 参数传递 指的是形参与实参结合的过程,其基本方式包括值传递和引用传递两种。 注意:形式参数的本质和局部变量相似,在进入函数的时候创建,退出函数的时候销毁。 ### 4.1 值传递 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。例如新建 swapxy.cpp,输入: ```C++ #include<iostream> using namespace std; void swap(int a,int b){ //定义空函数 swap,用于交换 a 和 b 的值。 int t=a; //定义中间变量 t,并将 a 的值赋给变量 t。 a=b; //将 b 的值赋给 a。 b=t; //将 t 的值赋给 b,又因为 t=a,所以把 a 的值赋给 b。 } int main(){ int x=3; int y=4; cout<<"x="<<x<<","<<"y="<<y<<endl; //输出 x、y。 swap(x,y); // 调用 swap 函数。 cout<<"x="<<x<<","<<"y="<<y<<endl; //输出调用函数后 x、y 的值。 }主函数中第一句` cout<<"x="<
程序结果: 4.2 引用传递
使用引用传递,需要在定义函数时,在参数前加上符号 &,该方法把参数的引用复制给形式参数,修改形式参数会影响实际参数。例如修改 swapxy.cpp 文件为以下内容:
#include<iostream> using namespace std; void swap(int &a,int &b){ //定义空函数 swap,用于交换 a 和 b 的值。 int t=a; //定义中间变量 t,并将 a 的值赋给变量 t。 a=b; //将 b 的值赋给 a。 b=t; //将 t 的值赋给 b,又因为 t=a,所以把 a 的值赋给 b。 } int main(){ int x=3; int y=4; cout<<"x="<<x<<","<<"y="<<y<<endl; //输出 x、y。 swap(x,y); // 调用 swap 函数。 cout<<"x="<<x<<","<<"y="<<y<<endl; //输出调用函数后 x 和 y 的值。 }函数 swap 的形参属于引用类型,所以修改形参将影响实参。
程序结果:shiyanlou:project/g++ swapxy.cpp -o swapxy shiyanlou:project/./swapxy X=3,y=4 X=4,y=3 shiyanlou:project/ $5. 拓展
5.1 默认参数
在 C++ 中,函数可定义具有默认值的参数,即 默认参数。当调用函数时,如果用户指定了参数值,则使用用户指定的值;如果参数值缺省,则使用默认参数。例如新建` defaultcs.cpp `,输入:
#include<iostream> using namespace std; int add(int a,int b = 5){//变量 b 的值默认为 5。 return a + b; } int main() { int a,b; cout<<"input a b:"<<endl; cin>>a>>b; cout<<"a + b(default) = "<<add(a)<<endl; //此时调用的 add 函数将 b 的值缺省,使之为默认参数。 cout<<"a + b = "<<add(a,b)<<endl; return 0; }add(a) 的返回结果为:输入的 a 值与默认 b 值之和。
add(a,b) 的返回结果为:输入的 a 值与输入的 b 值之和。
程序结果:shiyanlou:project/g++ defaultcs .cpp -o defaultcs shiyanlou:project/./defaultcs input a b: 2 3 a + b(默认值) = 7 a + b = 5 shiyanlou:project/ $5.2 函数重载
函数重载 指定义一个参数类型或参数个数不同的重名函数。当程序执行时,编译器将调用参数类型与函数形参类型一致的函数。例如新建` renamehs.cpp `,输入:
#include<iostream> using namespace std; int add(int a,int b){ //定义整型函数 add。 int c; c = a - b; return c; } double add(double a,double b){ //定义双精度型函数 add。 double c; c = a + b; return c; } int main() {//函数重载。 cout<<"a + b = "<<add(3,4)<<endl;//调用整型函数 add。 cout<<"a + b = "<<add(3.111,4.222)<<endl;//调用双精度型函数 add。 }执行` add(3,4) `时,调用` int add(int a,int b) `函数。
执行` add(3.111,4.222) `时,调用` double add(double a,double b) `函数。
程序结果:shiyanlou:project/g++ renamehs .cpp -o renamehs shiyanlou:project/ $./renamehs a + b = -1 a + b = 7.333 shiyanlou:project/ $6. 实验总结
本次实验学习了以下几个知识点:
- 函数的定义
- 函数的声明与调用
- 参数传递
- 默认参数
- 函数重载
此次实验我们以定义函数和调用函数为基础,介绍` 值传递 `和` 引用传递 `两种参数传递方式,拓展默认函数和函数重载的概念及基本操作。六、类和对象
1. 实验介绍
在现实生活中,对象可以是有形的,比如汽车、水果、蔬菜,也可以是无形的,比如一项计划。若对类似的对象进行抽象,找出其共同属性,则构成一种类型。本次实验涉及三个内容:面向对象程序设计的基本特点、类的定义和类成员的访问控制、对象的声明。
1.1 知识点
- 抽象、封装、继承、多态
- 类的定义
- 类成员访问控制
- 对象的声明
- 类的成员函数
2. 面向对象程序设计的基本特点
面向对象程序设计的基本特点包括:抽象、封装、继承、多态。
2.1 抽象
抽象是指对具体问题或对象进行概括,抽出其公共性质并加以描述的过程。一般情况抽象分为数据抽象和行为抽象,其中数据抽象是指一个对象区别于另一个对象的特征,行为抽象是指某类对象的共同特征。
2.2 封装
封装是指将抽象得到的数据和行为相结合,隐藏对象属性和实现细节且仅对外提供公共访问方式的过程。
2.3 继承
继承是指通过代码复用在保持原有类特性的基础上对子类进行具体说明的过程,通俗来说继承就是精简重复代码的手段。
2.4 多态
多态是指一段程序能够处理多种类型对象的能力,函数重载就是实现多态的一种手段。在 C++ 语言中多态可分为强制多态、重载多态、类型参数化多态和包含多态。
3. 类
类是 C++ 语言支持面向对象思想的重要机制,是实现封装的基本单元。
3.1 类的定义
类一般由类名、数据成员、函数成员三部分组成,定义类的基本形式为:
class 类的名称 { public: 外部接口 protected: 保护性成员 private: 私有成员 }其中 public、protected、private 表示访问控制属性,用于控制类成员的访问权限。特别的,没有分配控制属性的默认为 private 私有成员。
3.2 类成员访问控制
使用 public 关键字声明的公有成员可在类外访问,即公有类型成员定义了外部接口。例如新建 pubtest.cpp 文件,输入:
#include<iostream> using namespace std; class area //定义 area 类。 { public: //定义公有成员,外部可以访问。 double width; double length; }; int main() { area area1; //定义对象 area1。 double AREA; //定义双精度型变量 AREA。 area1.width=3; //外部访问公有成员,设置宽为 3。 area1.length=3.1; //外部访问公有成员,设置长为 3.1。 AREA=area1.width*area1.length; //计算面积。 cout << AREA <<endl; //输出面积 return 0; }此程序可以分为相对独立的两个部分,一是对 area 类的定义,二是主函数 main()。area 类定义了两个双精度型的公有成员,主函数访问类的公有成员实现数字相乘。
程序结果:shiyanlou:project/$ g++ pubtest.cpp -o pubtest shiyanlou:project/./pubtest 9.3 shiyanlou:project/ $使用 protected 关键字声明的受保护成员可在子类中访问。例如新建` protest.cpp `文件,输入:
#include<iostream> using namespace std; class area //定义 area 类。 { protected://定义受保护的成员。 double width; }; class squarearea:area //定义 squarearea 子类。 { public: //定义公有成员。 void setsquarewidth(double wid); //定义成员函数。 double getsquarewidth(void); }; //定义子类的成员函数。 void squarearea::setsquarewidth(double wid) { width=wid; //通过 squarearea 子类访问 area 类中受保护的成员 width。 } double squarearea::getsquarewidth(void) { return width; } //程序的主函数 。 int main() { squarearea square; //定义对象。 square.setsquarewidth(3.1); //设置宽度为 3.1。 cout <<"width:"<<square.getsquarewidth()<<endl; //输出设置的宽度。 return 0; }此程序可以分为相对独立的三个部分,一是对 area 类的定义,二是对 squarearea 子类及子类函数的定义,三是主函数 main()。对于 area 类中受保护的成员 width 可以通过子类进行访问。
程序结果:shiyanlou:project/ $ g++ protest.cpp -o protest shiyanlou:project/ $./protest width:3.1 shiyanlou:project/ $使用 private 关键字声明的私有成员只能被本类的成员函数访问。例如新建 pritest.cpp 文件,输入:
#include<iostream> using namespace std; class area { public: //定义公有成员 double length; void setwidth(double wid); double getwidth(void); private://定义私有成员 double width; }; //定义公有成员函数 void area::setwidth(double wid) { width=wid; } double area::getwidth(void) { return width; } //主函数 int main() { area len1; //定义对象 area wid1; len1.length=3.1; //访问 area 类公有成员。 cout<<"length:"<<len1.length<<endl; wid1.setwidth(3.2); //使用公有成员函数设置宽度。 cout <<"width:"<<wid1.getwidth()<<endl; return 0; }此程序可以分为相对独立的两个部分,一是 area 类及其公有成员函数的定义,二是主函数 main()。私有成员 width 只能先被类中成员函数访问,再通过相关的公共成员函数实现外部访问。若在类外部使用` area.width=3.2; `方式访问 area 类的私有成员 width,则会报错。
程序结果:shiyanlou:project/g++ pritest.cpp -o pritest6 shiyanlou:project/ $ ./pritest length:3.1 width:3.2 shiyanlou:project/ $name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/pubtest.cpp error: 未创建 pubtest.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep public /home/project/pubtest.cpp
error: 未使用 public 关键字声明公有成员
timeout: 2 - name: 检查目录是否创建1
script: |!/bin/bash
ls /home/project/protest.cpp
error: 未创建 protest.cpp 文件
timeout: 3 - name: 检查文件内容1
script: |!/bin/bash
grep protected /home/project/protest.cpp
error: 未使用 protected 关键字声明受保护的成员
timeout: 4 - name: 检查目录是否创建2
script: |!/bin/bash
ls /home/project/pritest.cpp
error: 未创建 pritest.cpp 文件
timeout: 5 - name: 检查文件内容2
script: |!/bin/bash
grep private /home/project/pritest.cpp
error: 未使用 private 关键字声明私有成员
timeout: 6## 4. 对象 在 C++ 语言中,对象是某类事物的实例。其声明形式为: ```C++ 类名 对象名;例如声明 area 类的对象 square:
area square;4.1 访问对象的成员
定义了类及其对象后,则可访问对象的成员。访问 数据成员 基本形式为:
对象名.成员名例如访问 area 类的对象 square 的数据成员 width:
square.width=3.2;访问 函数成员 的基本形式为:
对象名.函数名(参数表)例如访问 area 类的对象 square 的函数成员 setwidth(double wid):
square.setwidth(3.2);4.2 类的成员函数
成员函数描述了类的行为。成员函数在类中只用说明其函数的返回类型、名字和参数表,而函数的具体实现在类外说明,其基本形式为:
class 类名 { 函数的返回类型 函数名(参数列表) } 返回值类型 类名::函数名(参数列表) //特别的,在类外说明函数的具体实现时,需要使用类名限制成员函数,其形式为:类名::函数名(参数列表)。 { 函数体 } 例如在 area 类中有成员函数 void setwidth(double wid);: class area { ... void setwidth(double wid); ... } //在类外说明函数的具体实现 void area::setwidth(double wid) { 函数体 }5. 实验总结
本次实验学习了以下几个知识点:
- 抽象、封装、继承、多态
- 类的定义
- 类成员访问控制
- 对象的声明
- 类的成员函数
此次实验首先介绍了面向对象程序设计的基本特点,其次在定义类的基础上细解了 public、protected、private 三种访问控制属性,最后在声明对象的基础上说明了访问数据成员和函数成员的基本方式。七、数组、字符串和指针
1. 实验介绍
在 C++ 中可通过一些途径高效地处理连续存放的大量数据,以相对低的代价实现数据共享。本次实验共有五个内容:数组、多维数组、字符串、指针变量的声明和使用、区别指针和引用。
1.1 知识点
- 初始化数组和访问数组
- 多维数组
- 字符串
- 指针变量的声明、赋值和使用
- 指针和引用的区别
2. 数组
数组用于存储一系列类型相同的元素。例如定义五个类型相同的变量,可以不用定义五个变量,只需要将五个值存到一个数组中即可。
上图定义了一个包含五个元素的 int 数组。第一个元素的编号为 0,最后一个编号为 4;在 C++ 中,数组的第一个元素总是编号为 0。
数组的声明格式:type name[elements] //一维数组的声明格式。其中 type 为数组类型,name 为数组名,elements 表示数组长度。数组长度就是数组最多可以存放的元素个数。
声明不同类型的数组:int a[5]; //包含五个元素的 int 数组。 float a[5]; //包含五个元素的 float 数组。 char a[5]; //包含五个元素的 char 数组。2.1 初始化数组
默认情况下,数组中所有元素的值在声明数组时是未确定的。
若要在声明数组时,将数组中的元素初始化为特定的值,可以使用 {} 实现,例如初始化数组 int a[5]:int a[5] = {1,2,3,4,5}初始化结果为:
初始化数组时,若填充元素不完整,则剩下元素的值默认为 0。例如:
int a[5] = {1,2,3}其初始化结果为:
3. 访问数组
访问数组的表达式为:
name[index]其中 name 为数组名称,index 为元素编号。name[index] 称为数组下标。
例如定义且初始化一个数组:`int a[5] = {1,2,3,4,5}`。可以得到 a[0]=1,a[1]=2,a[2]=3,a[3]=4,a[4]=5。
用图形化方式更直观的表示此数组:则 a[2] 可以访问数组中的元素 3。
新建 arr1.cpp 文件,输入:#include<iostream> using namespace std; int main () { int a[5] = {1,2,3,4,5}; //初始化数组。 cout<<a[2]<<endl; //访问数组。 return 0; }程序结果:
shiyanlou:project/ $ g++ arr1.cpp -o arr1 shiyanlou:project/ $ ./arr1 shiyanlou:project/ $4. 多维数组
C++ 中支持多维数组,多维数组是指二维及以上的数组,其表达式为:
type name[index1][index2][index3]...[indexn]其中` index `的数量决定了数组的维度,例如` type name[index1][index2] `表示的则是一个二维数组。
4.1 二维数组
可以将二维数组看成一张二维表。定义一个二维数组 a[3][5]:
二维数组 a[3][5] 可以表示成一张三行五列的二维表,其初始化顺序为:从左到右,从上到下。上图的初始化顺序为:a[0][0]、a[0][1]、a[0][2]、a[0][3]、a[0][4]、a[1][0]…
其中绿色部分表示下标为 a[1][2] 的数组元素:新建` arr2.cpp `文件,输入:
#include<iostream> using namespace std; int main () { int a[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; //初始化数组。 cout<<a[1][2]<<endl; //访问数组。 return 0; }初始化数组后:
输出下标为 a[1][2] 的数组元素:
shiyanlou:project/ $ g++ arr2.cpp -o arr2 shiyanlou:project/ $ ./arr2 8 shiyanlou:project/ $二维以上的多维数组并不常用,在此暂不做介绍。
5. C-style 字符串
C++ 支持起源于 C 语言的 C-style 字符串。
例如定义一个可以容纳 10 个 char 型字符的数组:char ch[10];数组` char ch[10]; `可以表示为下图:
此数组具有存储 10 个字符的能力,但是这个容量不需要完全使用,数组也可以容纳更短的长度。
按照惯例,字符表示的字符串由特殊字符 null 结尾,其字面值可以写为 �。
例如在数组` char ch[10] `中存入一个` Hello `字符串:` char ch[10] = “Hello”; `。
可以表示为:注意:null 字符不用手动添加,C++ 在编译的时候会自动添加。
5.1 cstring
在 C++ 中有大量用于操作` C-style `字符串的函数,它们集成在头文件`
`中。其常见的函数:
| 函数 | 作用 |
| ————————– | ———————– |
| strcpy(s1,s2) | 复制字符串 s2 到 s1 |
| strcat(s1,s2) | 将字符串 s2 连接到 s1 末尾 |
| strlen(s) | 计算字符串 s 长度 |
| strcmp(s1,s2) | 比较字符串 s1 和 s2 的长度,相同为 0;s1 < s2 为 -1;s1 > s2 为 1 |
| strchr(s1,ch) | 返回一个指针,指向 s1 中字符 ch 第一次出现的位置 |
| strstr(s1,s2) | 返回一个指针,指向 s1 中字符串 s2 第一次出现的位置 |
新建` str1.cpp `文件,输入:#include <iostream> #include <cstring> using namespace std; int main() { char str1[18] = "Hello "; char str2[11] = "Shiyanlou!"; strcat(str1,str2);//连接字符串。 cout<<"连接 str1 和 str2:"<<str1<<endl; cout<<"连接 str1 和 str2 后 str1 的长度:"<<strlen(str1)<<endl; //计算字符串 str1 的长度。 return 0; }其中` strcat(str1,str2); `将字符串` str2 `连接到` str1 `末尾,输出` Hello Shiyanlou! `
程序结果:shiyanlou:project/ $ g++ str1.cpp -o str1 shiyanlou:project/ $ ./str1 连接 str1 和 str2: Hello Shiyanlou! 连接 str1 和 str2 后 str1 的长度:16 shiyanlou:project/ $name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/str1.cpp error: 未创建 str1.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep strcat /home/project/str1.cpp
error: 未将字符串 str2 连接到 str1 末尾
timeout: 2## 6. 字符串 C++ 标准库中提供了` string `类,它不仅支持` C-style `的所有操作,而且还丰富了其他功能。 <img src="assets/images/7600.png"> ### 6.1 初始化字符串 初始化字符串 str1: ```C++ string str1("Hello");上述将` str1 `初始化为` Hello `,其等效于:
string str1 = "Hello";注意:` string `不需要像数组一样指定元素个数,它会自动分配内存。
新建` str2.cpp `文件,输入:#include <iostream> using namespace std; int main() { string str1 = "Hello "; string str2 = "Shiyanlou!"; str1.append(str2);//连接字符串。 cout<<"连接 s1 和 s2:"<<str1<<endl; cout<<"连接 s1 和 s2 后 str1 的长度:"<<str1.length()<<endl; //计算字符串 str1 的长度。 return 0; }其中` str1.append(str2); `将字符串` str2 `连接到` str1 `末尾,输出` Hello Shiyanlou! `
程序结果:shiyanlou:project/ $ g++ str2.cppstr2 shiyanlou:project/ $ ./str2 连接 str1 和 str2: Hello Shiyanlou! 连接 str1 和 str2 后 str1 的长度:16 shiyanlou:project/ $在 C++ 中,` string `还支持常规操作符,如: ==,<,=,<=,!=,+,+=,[]。新建` str3.cpp `文件,输入:
#include <iostream> using namespace std; int main() { string str1 = "Hello "; string str2 = "Shiyanlou!"; cout<<"连接 str1 和 str2:"<<str1 + str2<<endl;//连接字符串。 return 0; }对于字符串来说,常规操作符 + 的功能为 连接。
程序结果:shiyanlou:project/$ g++ str3.cpp -o str3 shiyanlou:project/$ ./str3 连接 str1 和 str2: Hello Shiyanlou! shiyanlou:project/ $name: 检查目录是否创建 script: | #!/bin/bash ls /home/project/str2.cpp error: 未创建 str2.cpp 文件 timeout: 1 - name: 检查文件内容
script: |!/bin/bash
grep append /home/project/str2.cpp
error: 未将字符串 str2 连接到 str1 末尾
timeout: 2## 7. 指针 指针是 C++ 从 C 中继承的重要数据类型,它提供了一种较为直接的地址操作手段,使用指针可灵活实现动态内存的分配。 ### 7.1 指针变量的声明 指针是一种数据类型,具有指针类型的变量称为 指针变量,它用于存放内存地址。在使用指针之前,需对指针进行声明,其一般形式为: ```C++ type *pointer_name;其中` type `表示所指对象的数据类型,即说明所指内存单元存放的数据类型;` * ` 表示声明一个指针类型的变量;` pointer_name `表示指针名。
声明三个数据类型不同的指针变量:int *p; //声明 int 型的指针变量 p。 float *p1; //声明 float 型的指针变量 p1。 char *p2; //声明 char 型的指针变量 p2。值得注意的是,指针实际存放的是内存地址,不同类型的指针变量用于指向数据类型不同的变量或常量。
C++ 中提供了两个与地址相关的运算符: 和 & 。其中 称为 指针运算符,用于获取指针所指变量的值,例如 *p 表示指针 p 所指变量的值;& 称为 取地址运算符,用于获取一个对象的地址,例如有变量 i,则 &i 表示 i 的存储地址。8. 指针的赋值与使用
定义指针后我们仅得到了一个用于存储地址的指针变量,若要确定指针变量存放的内存地址,可以通过给指针赋值实现。其基本形式为:
- 在定义指针时为指针赋值:
`C++ type *pointer_name=初始地址;` - 在定义指针后为指针赋值:
type *pointer_name; pointer_name=初始地址;其中 初始地址 为某个对象的内存地址,一般使用 &对象名 获取。
例如将指针 p 指向变量 po1 的地址,其中变量 po1 的值为 6:int po1=6; //定义 int 型变量 po1,赋值为 6。 int *p=&po1; //指针变量 p 指向变量 po1 的地址。其等价于:
int po1=6; //定义 int 型变量 po1,赋值为 6。 int *p; //定义指针变量 p p=&po1; //指针变量 p 指向变量 po1 的地址。我们通过一个例子加深一下指针的定义和使用,新建 `pointer1.cpp`,输入:
#include<iostream> using namespace std; int main () { int po1=6; //定义 int 型变量 po1,赋值为 6。 int *p=&po1; //指针变量 p 指向变量 po1 的地址。 cout << "获取指针所指变量的值: "<<*p<<endl; cout << "获取指针的内存地址: "<<&p<<endl; return 0; }其中` p `运用指针运算符 * 获取指针 p 所指变量的值;` &p `运用取地址运算符 & 获取指针 p 的地址。
程序结果:shiyanlou:project/ $ g++ pointer1.cpp -o pointer1 shiyanlou:project/ $ ./pointer1 指针运算符 : 6 取地址运算符 : @x7ffe060ob5609. 引用
引用是指对已存在的变量别名,我们可以使用引用名来指向对象。
引用与指针主要有三个区别:
- 可以通过` 指针名=0 `描述一个空指针,但不存在空引用。
- 指针可在任何时间进行初始化操作,而引用只能在定义时进行初始化操作。
- 指针变量指向内存的一个存储单元;而引用只不过是原变量的一个别名而已。
声明引用变量的基本格式为:type &引用名=被指对象名;& 在这里是标识作用,而非取地址运算符。
例如定义引用 x,它是整型变量 i 的引用:int &x=i;我们通过 初始化时间 来区别一下指针和引用,新建 `pointer2.cpp` 文件,输入:
#include<iostream> using namespace std; int main () { int i=3; int j=4; int &x=i; //定义引用 x,它是整型变量 i 的引用。 int *s; //定义指针 s。 s=&j; //指针 s 指向整型变量 j 的地址。 cout << "初始化引用 x: " << x << endl; cout << "初始化指针 s: " << *s << endl; return 0; }其中 int &x=i; 表示在 定义引用时 进行的初始化操作。s=&j; 表示在 定义指针后 进行的初始化操作。
程序结果:shiyanlou:project/ $ g++ pointer2.cpp -o pointer2 shiyanlou:project/ $ ./pointer2 初始化引用x: 3 初始化指针 s: 4 shiyanlou:project/ $10. 实验总结
本次实验学习了以下几个知识点:
- 初始化数组和访问数组
- 多维数组
- 字符串
- 指针变量的声明、赋值和使用
- 指针和引用的区别
此次实验我们在声明数组和访问数组的基础操作上,细解了多维数组中的二维数组,其次基于` C-style `字符串和` string `介绍了 C++ 中字符串的基本操作,最后在介绍指针声明与使用的基础上,区别了指针和引用。八、C++面试清单(附答案)
1、简单介绍一下你的项目?
这个项目主要目的是对浏览器的链接请求进行解析处理,处理完之后给浏览器客户端返回一个响应,如文字图片视频等。服务器后端的处理方式使用socket通信,利用IO多路复用具体采用Epoll,可以同时处理多个请求,请求的解析使用预先准备好的线程池,使用Reactor模式,主线程负责监听,当监听到有事件之后,将事件封装成一个请求对象插入请求队列。睡眠在请求队列上的工作线程被唤醒进行读取数据和逻辑处理。处理的方式用有限状态机。
对于请求文件的处理:客户端发出链接,到达服务器,服务器这端先用read_once()函数一次性把所有请求读到缓冲区。然后process_read()函数分别调用三个函数对缓冲区的内容进行解析。主状态机主要用于解析客户端请求,从状态机采用正则表达式用于解析每一行的内容,以方便主状态机的解析,主状态机调用解析请求行、请求头、请求体三部分函数进行解析。解析结束后利用do_request()函数生成相应报文,该函数会根据不同的网址url产生不同的响应体通过文件映射的方式映射到内存。最后通过writev()函数里的套接字的传输方式把响应体传给客户端。
日志则分为同步日志和异步日志,该项目采用异步日志,异步日志利用阻塞队列,先将日志放入阻塞队列中,然后利用条件变量将日志添加到对应文件中。采用单例模式。
日志系统初始化函数主要做的事:如果文件名没带路径,直接放到log_full_name。如果带路径,把文件名取出来放到log_name,把路径取出来放到dir_name,然后把时间+log_name放到log_full_name。
在write_log()函数中,这里面有两个部分,一部分是对新的文件名、时间、日志名进行再次处理,一部分是时间+新加入的日志参数放入缓冲区。异步加入阻塞队列,同步直接写入日志文件。阻塞队列采用双向队列实现的生产者-消费者模式。根据初始化函数传入的最后一个参数即阻塞队列的最大容量值来判断是同步还是异步,异步则是大于等于1。
采用单例模式创建了数据库连接池。在数据库连接池中,会提前创建好一定量的数据链接,并把他们保存在队列中。
采用定时器处理非活动链接,定时器容器利用小根堆进行设计实现。2、web服务器你自己是否申请了域名,域名号是多少?
没有申请域名。
租了一台云服务器,因为有公网IP,所以只需要通过公网IP + 端口号就可以访问。3、手写线程池?
C++实现简易线程池,包含线程数量、启动标志位、线程列表、条件变量。
其中构造函数主要是声明未启动和线程数量的。start函数为启动线程池,将num个线程绑定threadfunc自定义函数并执行,加入线程列表。stop是暂时停止线程,并由条件变量通知所有线程。析构函数是停止,阻塞所有线程并将其从线程列表剔除后删除,清空线程列表。#include <mutex> #include <condition_variable> #include <thread> #include <vector> #include <stding> #include <list> using namespace std; class ThreadPool { public: ThreadPool(int threadnum) : started(false), thread_num(threadnum) {} ~ThreadPool() { stop(); for (int i = 0; i < thread_num; ++i) { threadlist[i]->join(); } for (int i = 0; i < thread_num; ++i) { delete threadlist[i]; } threadlist.clear(); } void threadFunc() {} // 线程执行函数,可自定义 void start() { // 启动线程池 if (thread_num > 0) { started = true; for (int i = 0; i < thread_num; ++i) { thread* pthread = new thread(&threadFunc, this); threadlist.push_back(pthread); } } } void stop() { started = false; condition.notify_all(); } private: int thread_num; // 线程数量 bool started; // 是否开启 vector<thread*> threadlist; // 线程列表 condition_variable condition;// 信号量 };4、C++用过什么设计模式?讲一讲
在设计日志系统和数据库连接池的时候,用过单例模式。
单例模式的目的是,用户在调用该类的时候,只能建立一个该类对象。于是就将该类的构造函数给私有化了,这样外部就根本没办法直接实例化调用该类,只能类内部调用。这时候就在类内创建一个公有化的函数,然后让该函数返回一个该类的指针,这样外部就可以通过这个函数调用该类了。但是问题是,调用该成员函数就必须要实例化一个该类对象,而现在已经不能实例化该类对象了,所以为了可以成功调用该函数,把该函数设置为静态函数,静态函数的作用范围是全局整个文件,这样外部就可以调用了。
但是这样的话,实际上并不能保证主函数调用时该类对象指针的唯一性,因为该静态函数每次返回的都是一个新的new出来的值。每次都不一样。于是办法就是设置一个私有化的静态对象指针,在外部初始化这个指针为空。在静态成员函数中,如果该静态指针为空就创建对象指针,否则直接返回对象指针。这样就确保在外部使用时,该对象的唯一性。
但是问题又来了,这样做可能会导致内存泄漏,因为你静态成员函数申请的指针所指向的内存并没有释放,还需要用户手动释放。改动的话就把静态成员函数中创建指针改为创建一个静态对象成员,然后返回该成员的地址。
但是返回一个地址就需要用指针去接收,用户就有可能对该指针进行delete造成错误,所以直接静态成员函数返回一个引用更好。这样delete就会无效。
但是引用之后,主函数可以通过赋值号(拷贝构造函数)产生新的类对象。突破了唯一性的设定。所以又需要对拷贝构造函数进行私有化设置。或者直接对拷贝构造函数 = delete,进行禁用。亦或把默认的运算符重载给禁用了。#include <iostream> #include <algorithm> #include <stdlib.h> #include <vector> #include <string> using namespace std; class Singleton { public: ~Singleton() { } static Singleton& CreateObject() { static Singleton obj; return obj; } private: Singleton() { } Singleton(Singleton& obj) { } }; int main() { Singleton& obj1 = Singleton::CreateObject(); // 实际上使用Singleton类中的obj静态变量 return 0; }5、线程同步机制有哪些?
现在流行的进程线程同步机制,其实是由最原始、最基本的4种方法实现的。
临界区:通过多线程的互串行访问公共资源或一段代码,速度快,适合控制数据访问。互斥量(互斥锁):为协调共同对一个共享资源(临界区)的单独访问而设计。只有拥有互斥对象的线程才有权限去访问系统的公共资源,因为互斥对象只有一个,所以能够保证资源不会同时被多个线程去访问。信号量:为控制一个具有有限数量的用户资源而设计。它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数。事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。条件变量。6、线程池中的工作线程是一直等待吗?
线程池中的工作线程是处于一直阻塞等待的模式下。因为在我们创建线程池之初时,我们通过循环调用pthread_create往线程池中创建了8个工作线程,工作线程处理函数接口为pthread_create函数原型中第三个参数即函数指针所指向的worker函数(自定义的函数),然后调用线程池类run(自定义)。
为什么不直接指向run函数,而是需要通过worker中传入对象从而调用run呢?
原因是我们已经将worker设置为静态成员函数(工作线程共享worker函数),而静态成员函数只能访问静态成员变量,所以为了能够访问到类内的非静态成员变量,我们可以在worker中调用run这个这个非静态成员变量来达到这一要求。在run函数中,我们为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上,因此项目中的线程池中的工作线程是一直处于阻塞等待模式下的。7、线程池工作线程处理完一个任务的状态是什么?
分两种情况考虑:
- 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态。
- 当处理完任务后如果请求队列不为空时,则这个线程将处于与其它线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。
8、如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?
首先这种问法就相当于问服务器是如何处理高并发问题。
本项目是通过对子线程的循环调用来解决高并发的问题,具体实现过程如下:
我们在创建线程的同时就调用pthread_death将线程进行分离,这样就不用单独对工作线程进行回收,但是一般情况下我们只要设置了分离属性,那么这个线程在处理完任务之后,也就是子线程结束后,资源会被自动回收。这种情况下我们服务器最多只能同时处理8个请求事件了(线程池中只有8个线程)。那怎么实现高并发的请求呢?可能会说让线程池中创建足够多的线程,但这只是理想化的,而现实中线程数量过大会导致更多线程的上下文切换,占用更多的内存,所以显然是不合理的。而本项目中是通过子线程的run的调用函数进行while循环,让每一个线程池中的工作线程都不会终止,即让它处理完当前任务就去处理下一个,没有任务就一直阻塞等待。以此达到服务器高并发的要求,同一时刻8个线程都会处理请求任务,处理完之后就接着处理,直到请求队列为空为止。9、如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢?有什么好的应对策略呢?
会影响接下来的客户请求,因为线程池内的线程的数量是有限的,如果客户请求占用线程时间过久的话会影响处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的请求。
应对策略:
可为线程处理请求对象设置处理的超时时间,超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则可直接将其断开连接。10、http连接处理
在本模块中,主要分析http请求与响应这两个部分。
- http连接请求处理
在启动服务器时,先创建好线程池。当浏览器端发出http请求时,主线程创建http类对象数组用来接收请求并将其所有数据读入各自对象对应的buffer中,然后将该对象插入任务队列中;如果是连接请求,那么就将它注册到内核事件表中。线程池中的工作线程从任务队列中取出一个任务进行处理(解析请求报文)。 - http报文解析处理流程
各工作线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。同时项目中也加入了主从状态机的使用,状态机根据当前的状态来做特定功能的任务。其中从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。由于HTTP报文中,每一行数据由rn作为结束符,空行则直接是rn。因此通过查找rn将报文拆解成单独的行进行解析。
从状态机负责读取buffer中的数据,每行数据末尾的rn置为��,并更新从状态机在buffer中读取的位置,以此来驱动主状态机进行解析。主状态机初始状态是CHECKSTATEREQUESTLINE,然后通过从状态机来驱动主状态机以改变主状态机的状态。process_read通过while循环,将主从状态机进行封装,对报文的每一行循环处理。在循环体中从状态机读取数据,同时将读取到的数据间接赋给text缓冲区,然后利用主状态机来解析吗text中的内容。
整体流程总结:客户端发出链接,到达服务器,服务器这块先用read_once()函数一次性把所有请求读到缓冲区。然后process_read函数分别调用三个函数对缓冲区的内容进行解析。主从状态机主要用于解析客户端请求,从状态机用于解析一行内容并把每一行加��格式化,方便主状态机解析,主状态机调用解析请求行、请求头、请求体内容三部分函数进行解析。解析结束后利用do_request()函数生成响应报文,该函数会根据不同的网址url产生不同的响应体。最后通过write函数,利用套接字传输的方式把响应体传给客户端。11、为什么要用状态机?
因为传统应用程序的控制流程基本是按顺序执行的:遵循事先设定的逻辑,从头到尾地执行。简单来说如果想在不同地状态下实现代码地跳转,就需要破坏一些代码,这样就会造成代码逻辑地混乱,代码也会显得十分地复杂。所以我们必须采取不同地技术来处理这些情况。它能处理任何顺序的事件,并能提供有意义的响应—-即使这些事件发生的顺序和预计的不同,有限状态机正是为了满足这方面的要求而设计的。每个状态都有一系列的转移,每个转移与输入和另一个状态相关。当输入进来,如果它与当前状态的某个转移相匹配,机器转换为所指的状态,然后执行相应的代码。
12、https协议为什么是安全的?
https = http + TLS/SSL。
TLS/SSL协议位于应用层协议和TCP之间,构建在TCP之上,由于TCP协议保证数据传输的可靠性,任何数据到达TCP之前,都需要经过TLS/SSL协议的处理。
https是加密传输协议,可以保障客户端到服务器端的数据传输的安全。用户通过http协议访问网站时,浏览器和服务器之间是明文传输的,这就意味着用户填写的密码、账号、交易信息等机密信息都是明文的,随时可能被泄漏、窃取、篡改,被第三方者加以利用。安装SSL证书后,使用https加密协议访问网站,可激活客户端浏览器到网站服务器之间的 “SSL加密通道(SSL协议)”,实现高强度双向加密传输,防止传输数据被泄漏或者被篡改。13、https的SSL连接过程?
- 客户端提交https请求。
- 服务器响应客户,并把证书公钥发给客户端。
- 客户端验证证书公钥的有效性。
- 有效后,会生成一个会话密钥。
- 用证书公钥加密这个会话密钥后,再发送给服务器。
- 服务器收到公钥加密的会话密钥后,用私钥来解密,得到会话密钥。
- 客户端与服务器双方利用这个会话密钥加密要传输的数据进行通信。
14、GET和POST的区别?
请求方式 GET POST 参数位置 url在query中 一般在content中,query也可 参数大小 受限于浏览器url大小,一般不超过32k 1G 服务器数据接收 接收1次 根据数据大小,可分多次接收 使用场景 从服务器获取数据,不做增删改 向服务器提交数据,做增删改 语义 参数带在url中,安全性低 相对于GET请求,安全性更高 15.服务器使用的并发模型?
参考《Linux高性能服务器编程》P130-P136
- 前置知识
IO操作:IO即Input和Output的简写,狭义上是读写硬盘的操作。广义上则是指不需要CPU参与的都是IO操作。在计算机科学中指计算机之间或人与计算机之间的信息交换。如两台计算机通过网卡进行交互、向硬盘写入数据或者读取硬盘数据、读取鼠标或者键盘输入、读写文件、访问数据库等等,都是IO操作。IO操作一般CPU消耗很少,但是耗时较长,任务的大部分时间都在等待IO操作完成(IO速度远低于CPU和内存的速度),所以当出现IO操作时,CPU都会异步的去执行其它事情。 - 半同步/半异步模式
同步异步(区别于线程同步、IO模型(事件处理模式)):按顺序依次执行程序就是同步,当程序的执行是由信号、中断等来驱动执行,则为异步。16.reactor、proactor、主从reactor模型的区别?
参考《Linux高性能服务器编程》P127-P130
- Reactor是(同步IO模型) :
主线程往epoll内核上注册socket读事件,主线程调用epoll_wait等待socket上有数据可读,当socket上有事件可读时,主线程把socket可读事件放入请求队列。睡眠在请求队列上的某个工作线程被唤醒,读取数据并处理客户请求,然后往epoll内核上注册socket写请求事件。主线程调用epoll_wait等待写请求事件,当有事件可写时,主线程把socket写事件放入请求队列。睡眠在请求队列上的工作线程被唤醒,处理客户请求。 - Proactor是(异步IO模型):
主线程调用aio_read函数(内核读取数据)向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读完成后如何通知应用程序,主线程继续处理其它逻辑,当socket上的数据被读入用户缓冲区后,通过信号告知应用程序数据已经可以使用。应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后调用aio_write函数(内核发送数据)向内核注册socket写完成事件,并告诉内核写缓冲区的位置,以及写完成时如何通知应用程序。主线程处理其它逻辑。当用户缓存区的数据被写入socket之后内核向应用程序发送一个信号,以通知应用程序数据已经发送完毕。此时应用程序预先定义好的数据处理函数就会完成工作。
reactor模式为同步I/O模式,注册对应读写事件处理器,等待事件发生进而调用事件处理器处理事件。而proactor模式为异步I/O模式。
reactor和proactor模式的主要区别就是真正的读取和写入操作是由谁来完成的,reactor中需要工作线程自己读取和写入数据,proactor模式中,应用程序不需要进行实际的读写操作,读写操作是由内核完成的。reactor: 同步网络模型,可以理解为:来了事件我通知你,你来处理。proactor:异步网络模型,可以理解为:来了事件我来处理,处理完了我通知你。理论上,proactor比reactor效率要高一些。
- 模拟proactor模式
使用同步I/O方式模拟出proactor模式的原理是:主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一”完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读的结果进行逻辑处理。
使用同步I/O模型(epoll_wait为例)模拟出proactor模式的工作流程:
主线程往epoll内核事件表中注册socket上的读就绪事件。主线程调用epoll_wait等待socket上有数据可读。当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象插入请求队列。睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表注册socket写就绪事件。当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
重点:
读就绪事件:当有事件到来,epoll_wait()单纯通知主线程有事件来了,主线程把事件放入请求队列。应用程序利用工作线程通过read()等函数把数据从内核缓冲区读到用户缓冲区(reactor)。
读完成事件:当有事件到来,主线程往内核注册这个读时间(告诉内核注意一会要读数据)。注册之后,主线程就去干其它事了,内核就自动会负责将数据从内核缓冲区放到用户缓冲区。而不需要用户程序管。
对于reactor模式模拟proatcor模式来说,之前proactor是主线程调用aio_read()函数向内核注册读事件,而在这里它主线程使用epoll向内核注册读事件。但是这里的内核不会负责将数据从内核读到用户缓冲区,最后是需要依靠主线程也就是用户程序read()函数等负责将内核数据循环读到用户缓冲区。对于工作线程来说收到的是已读完成的数据,模拟也就体现在这里。

































