程序设计原理复习笔记

17459 字
87 分钟
程序设计原理复习笔记
2026-01-13
2026-01-15

天津大学智算大一专业必修课。讲的依托答辩,学分占比又高,只能自己看ppt批注复习。


程序设计概述和c++基础知识#

“课程目标”:掌握结构化程序设计方法;选择合适的开发环境、工具,并能熟练使用该工具完成程序设计工作;培养学生的逻辑思维能力,培养其严谨的思维方式和良好的程序设计风格。

程序运行起来的内部过程#

graph TD A[源代码] --> |编译器:预处理、编译、汇编| B[机器码] B ---> |链接| C[可执行文件] C ---> |运行| D[结果]

变量 (Variable) 声明要求#

  • 由字母、数字、下划线组成。
  • 不能以数字开头。
  • 不能是保留字。
  • 长度不限,但最好控制在31字符以内。

变量声明#

int a = 1;
double b = .1e2;
char c = 'A';

最基础的输入/输出:cin / cout#

使用<iostream> 头文件。

#include <iostream>
using namespace std;
int main(){
char a[10];
cin >> a ; // 假设输入 "Hello"
cout << a << " world" << endl; //输出 "Hello world"
return 0;
}

数学方法(使用<cmath>头文件)#

三角函数(Trigonometric Functions)#

函数名作用调用与结果
cos计算余弦值cos(PI / 6) returns 0.866
sin计算正弦值sin(270 * PI/180) returns -1.0
tan计算正切值省略
asin计算反正弦asin(0.5) returns 0.523599(PI/6)
acos计算反余弦省略
atan计算反正切省略

上面的三个反三角函数都返回弧度值。

指数函数(Exponent Functions)#

函数名作用调用与结果
exp计算自然指数的n次方exp(1.0) returns 2.71828
log计算n的自然对数log(E) returns 1.0
log10计算n的常用对数log10(10.0) returns 1.0
pow (m,n))计算m的n次方pow(2.0,3) returns 8.0
sqrt计算n的平方根sqrt(4.0) returns 2.0

不知道为什么不用ln反而用log…

取整函数(Rounding Function)#

函数名作用调用与结果
ceil向上取整ceil of 2.3 is 3.0, ceil of -2.3 is -2.0
floor向下取整floor of 2.3 is 2.0, floor of -2.3 is -3.0
fmod计算分子/分母的浮点余数(向零舍入)fmod of 5.3 / 2 is 1.300000

数字类数据类型以及占用字节数#

数据类型范围占用空间
short-2^15 ~ 2^15 - 116bit / 2byte
unsigned short0 ~ 2^16 - 116bit / 2byte
int-2^31 ~ 2^31 - 132bit / 4byte
unsigned int0 ~ 2^32 - 132bit / 4byte
long-2^31 ~ 2^31 - 132bit / 4byte
unsigned long0 ~ 2^32 - 132bit / 4byte
float~32bit / 4byte
double~64bit / 8byte
long double~80bit / 10byte

上面的各种占用空间根据系统和编译器的不同存在差异,比如 long 在 64 位 Unix/Linux 系统中通常为 8 字节,而在32位系统和所有Windows系统中为4字节。不必记忆,仅供参考。

long double 可精确到小数点后19位。


变量声明与基本运算#

基本声明方式#

一般声明:

const datatype CONSTANTNAME = VALUE;

直接替换:

#define identifier value

溢出#

上溢(overflow) : 超过数据类型的上限。

short a = 32767 + 1 ==> -32768

下溢(underflow) : 低于数据类型的下限。

short a = -32768 - 1 ==> 32767

算术运算符(Numeric Operator)#

NameMeaningExampleResult
+Addition34 + 135
-Subtraction34.0 - 0.133.9
*Multiplication300 * 309000
/Division1.0 / 2.00.5
%Remainder20 % 32

整数除法 (Integer Division)和取模运算(Remainder Operator)#

5 / 2 yields an integer 2. 5.0 / 2 yields a double value 2.5 5 % 2 yields 1 (the remainder of the division)

输出结果格式化(小数点位数)#

使用 <iomanip> 头文件。

double amount = 12618.98;
double interestRate = 0.0013;
double interest = amount * interestRate;
cout << "Interest is:"<<interest<<endl;
Interest is: 16.4047
cout << "Interest is:" <<fixed<<setprecision(2)<<interest<<endl;
Interest is: 16.40

setprecision() 设置的是小数点位数,而非保留有效数字位数。如果没有 fixedsetprecision(n) 默认设置的才是总的有效数字位数。

计算时的隐式类型转换规则#

当两个不同类型的变量进行运算时,c++会自动按照一定的优先级规律转换为一致类型。

简化后的转换规律如下:

  • 如果其中一个为浮点(long double / double / float),则优先转换为一致浮点。
  • 如果两个都为浮点且不同,则按照 long double > double > float 的规则,将低等级变量转换为高等级变量。
  • 如果两个都为定点(unsigned long / long / unsigned int / int)且不同,则按照 unsigned long > long > unsigned int > int 的规则,将低等级变量转换为高等级变量。

一般情况下的类型转换#

隐式转换:

double d = 3; // int被自动提升为float;仅适用于低等级转换为高等级

显式转换:

int i = static_cast<int>(3.0); // (type narrowing) (类型缩窄)
int i = (int)3.9; // (Fraction part is truncated) (小数点后被丢弃)

这种类型转换并不会改变原变量的类型。

double d = 4.5;
int i = static_cast<int>(d);  // d is not changed

增强型赋值运算符 (Augmented Assignment Operators)#

OperatorExampleEquivalent
+=i += 8i = i + 8
-=f -= 8.0f = f - 8.0
*=i *= 8i = i * 8
/=i /= 8i = i / 8
%=i %= 8i = i % 8

自增/自减运算符 (Increment and Decrement Operators)#

OperatorNameDescription
++varpreincrementThe expression (++var) increments var by 1 and evaluates to the new value in var after the increment.
var++postincrementThe expression (var++) evaluates to the original value in var and increments var by 1.
—varpredecrementThe expression (—var) decrements var by 1 and evaluates to the new value in var after the decrement.
var—postdecrementThe expression (var—) evaluates to the original value in var and decrements var by 1.

简单来说: ++var—var 先对自己的值+1 / -1 ,然后将运算后的值提交给表达式。 var++var— 先将自己的值提交给表达式,然后自己的值+1 / -1 。

Warning

永远不要写出诸如 int k = ++i + i诡异代码,这在c++标准中属于未定义行为!

Tip

如果你看的教材或习题上有这类让人摸不着头脑的代码,扔了它吧。


字符变量与逻辑运算#

数据类型#

静态类型与动态类型的比较(推广到语言层面,c++与python的比较)#

AspectC++ StaticPython Dynamic
Type BindingDecided at compile time; variables have fixed types.Decided at runtime; variables can reference any type.
Type CheckingCompile-time errors for invalid operations; earlier feedback.Runtime errors if an operation is incompatible; later feedback.
ConversionControlled implicit/explicit casts; predictable semantics.Frequent automatic coercion; convenient but may hide mistakes.
PerformanceOptimized by the compiler; tight memory layout and speed.Interpreter/VM overhead; flexible but generally slower.
Typical PitfallsOverly strict conversions; verbose type annotations.Type surprises at runtime; latent bugs in untested paths.

翻译版本:

方面C++ 静态类型Python 动态类型
类型绑定在编译时决定;变量有固定类型。在运行时决定;变量可以引用任何类型。
类型检查无效操作在编译时报错;反馈更早。操作不兼容时在运行时出错;反馈较晚。
类型转换受控的隐式/显式转换;语义可预测。频繁自动强制转换;方便但可能隐藏错误。
性能编译器优化;内存布局紧凑,速度快。解释器/虚拟机开销;灵活但通常较慢。
典型问题过度严格的转换;冗长的类型注解。运行时类型意外;未测试路径中的潜在错误。

•  In static typing, the compiler knows the shape of data and the valid operations ahead of time. •  In dynamic typing, flexibility is traded for potential runtime surprises.

定义#

数据类型定义两个方面:数据如何存储,哪些运算对其有效。

  • 不同的数据类型支持不同的运算;
  • 一些数据类型可以转换为其他数据类型,而另一些则不行;
  • 不同的类型会影响表达式及其语义。

本章节仅介绍和讨论char和bool类型。

ASCII码字符集#

ASCII码表是unicode表从 \u0000\u007f 的子集。(可能你不需要知道这些,仅供补充)

快速记忆: 0对应48,A对应65,a对应97. 小写字母的编号比其对应的大写字母大32. 0-9对应48-57,A-Z对应65-90,a-z对应97-122.

字符数据类型(Char)#

  • 一个char在底层占据1byte空间,是一种整数类型。(不要与int混淆!)
  • 它与ASCII码一一对应。

考虑到上述特点,char类型也可以参与一些算数和关系运算(arithmetic and relational operations),例如加法、减法(subtraction)、比大小。

char ch = 'A';
cout << ch << " " << int(ch) << endl;
ch++;
cout << ch << " " << int(ch) << endl;

字符和数字类型也可以在一定程度上互相转换。

int i = 'a'; // Same as int i = (int)'a';
char c = 97; // Same as char c = (char)97;
char c = 0xFF41;
cout<<c<<endl;
char c = 65.25;
cout<<c<<endl;
int i = ‘A’;
cout<<i;

特殊字符的转义序列(Escape Sequences for Special Characters)#

字符名称ASCII 码
\b退格 (Backspace)8
\t制表符 (Tab)9
\n换行 (Linefeed)10
\f换页 (Foremfeed)12
\r回车 (Carriage Return / Enter)13
\反斜杠 (Backslash,用于转义)92
单引号(Single Quote)39
双引号(Double Quote)34
cout<<“He said \"programming is fun!\""<<endl;
cout<<"\\t is a tab character"<<endl;
cout << "A\tB\tC\n1\t2\t3\n";

可能并非全部要求记住。

关系运算符(Relation Operators)#

OperatorNameExampleResult
<less than( 1 < 2 )true
<=less than or equal to( 1 <= 2 )true
>greater than( 1 > 2 )false
>=greater than or equal to( 1 >= 2 )false
==equal to( 1 == 2 )false
!=not equal to( 1 != 2 )true

布尔数据类型(The bool Data Type)#

bool lightOn = true;

非黑即白,非真(true)即假(false)。

c++在内部使用1代表真值,0代表假值。

逻辑运算符#

OperatorNameLogical Meaning
!notlogical negation (逻辑非)
&&andlogical conjunction (逻辑与)
||orlogical disjunction (逻辑或)

&&在两侧值都为真时返回真,否则返回假;(即:两侧全都是真才是真) ||在两侧值都为假时返回假,否则返回真;(即:有一侧为真就是真)

&&|| 两者均存在一套短路逻辑: 对于&& ,如果左侧值为假,则直接返回0,不继续计算右侧; 对于|| ,如果左侧值为真,则直接返回1,不继续计算右侧。 这套逻辑在计算复杂的优先级时需要着重考虑。

逻辑与的优先级高于逻辑或。

运算符优先级#

类别运算符结合性
后缀() [] -> . ++ --从左到右
一元+ - ! ~ ++ -- (type) * & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR|从左到右
逻辑与 AND&&从左到右
逻辑或 OR||从左到右
条件?:从右到左
赋值= += -= *= /= %= >>= <<= &= ^= |=从右到左
逗号,从左到右

(简单记忆) 在C++中,这些运算符的优先级从高到低大致如下:

  1. 后缀自增/自减(i++、i—)优先级最高。
  2. 前缀自增/自减(++i、—i)和引用(&)、解引用(*)都属于一元运算符,属于同一优先级,并且结合性从右到左。
  3. 乘除取模(*、/、%)优先级次之。
  4. 加减(+、-)优先级再次之。
  5. 比较运算符(<、<=、>、>=)等。
  6. 相等性运算符(==、!=)。
  7. 逻辑与(&&)。
  8. 逻辑或(||)。
    另外,逻辑与(&&)和逻辑或(||)具有短路特性。
Caution

运算符优先级决定的是表达式的结合方式,而不是求值顺序!
例如, a || ++b && 0 等价于 a || (++b && 0) , 先计算a的值,如果a为真,表达式直接返回1,右半部分不会计算!

#include <iostream>
int main() {
    int a = 5;
    int b = 3;
    int c = 2;
    int d = a + b * c -!a && c++ % a;
    std::cout << "d = " << d << std::endl;
    std::cout << "c after evaluation = " << c << std::endl;
    return 0;
}

聪明的你,能得出d的正确结果吗?


分支结构#

三种基本程序控制结构#

顺序结构(Sequential)、分支结构(Selection / Branching)、循环结构(Looping / Iteration)。

分支结构:if-else#

if (booleanExpression) {
  statement(s)-for-the-true-case;
}
else {
  statement(s)-for-the-false-case;
}

几类不同的分支结构(注意else if的用法)

分支结构:switch#

switch (status) {
  case 0:  compute taxes for single filers;
           break;
  case 1:  compute taxes for married file jointly;
           break;
  case 2:  compute taxes for married file separately;
           break;
  case 3:  compute taxes for head of household;
           break;
  default: cout <<Error: invalid statud!”;
}
  • switch 表达式必须返回 char、short 或 int 类型的值 (示例伪代码中的 status ),并且必须始终用括号括起。
  • case 后面必须紧跟一个常量表达式,这意味着他们不能包含变量,比如1+x.
  • break 关键字在理论上是可选的,但应在每个 case 语句末尾使用,以终止 switch 语句的后续部分。若未出现 break 语句,则会执行下一个 case 语句。
  • default情况可选,用于执行 status 未能满足任意一个 case 的情况。
  • 每个case按照自上而下的顺序进行判断,但case的顺序无关紧要(毕竟一般不会出现重复,且case个数有限,单纯判断过程对效率几乎无影响)。然而,按照顺序排列case是一种好的代码风格。

总结#

结构/Structure特点 / Feature适用场景 / When to Use注意事项 / Notes
if单分支 / Single branch只在条件为真时执行 / Do something only when condition is true建议总写花括号 {} / Always use braces
if - else双分支 / Two-way choice必须处理两种结果 / Must handle both outcomes注意边界与覆盖 / Watch boundaries and coverage
if - else if - else多分支梯形 / Multi-way ladder连续区间或多层条件 / Ranges or layered conditions从高到低判断 / Order matters (top-down)
switch多值匹配、结构清晰 / Discrete value matching整型/字符/枚举的等值判断 / Equality on int/char/enum每个 case 后写 break; 理解 fall-through / Use break; to avoid fall-through

循环结构#

循环结构:while#

while (loop-continuation-condition){
  // loop-body;
  Statement(s);
}

示例:

int count = 0;
while (count < 1000){
  cout << "Hello World!\n";
  count++;
}

注意:如果没有那行 count++ ,该循环将成为死循环。除非你确定代码逻辑需要,否则避免死循环可能带来的调试困难!

上述代码执行了1000次。如果将 < 改为 <= ,则代码将执行1001次。

Caution

永远不要使用浮点类型作为循环判断的条件!

while循环的执行原理#

flowchart TD Start([开始]) --> Condition{Loop Continuation Condition?} Condition -- true --> LoopBody[Loop body] LoopBody --> Condition Condition -- false --> End([结束])

break 和 continue#

break表示结束该段代码的所有循环运行,程序开始执行循环括号后的代码。
continue表示结束本轮循环运行,程序跳转到循环开头,执行新一轮的循环。

循环结构:do-while#

do
{
  // Loop body;
  Statement(s);
} while (loop-continuation-condition);

示例:

int n=10000;
int sum=0;
int counter=1;
while(counter<=n)
{
   sum=sum+counter;
   counter++;
}
do
{
   sum=sum+counter;
   counter++;
} while(counter<=n); // 两种不同的语法结构

do-while和while的区别#

  • while循环先判断条件,后执行循环体;而do-while先执行循环体,后判断条件。(管他的,先do了再说
  • 当条件一开始就不满足时,while循环一次也不执行,而do-while循环至少执行一次。
  • 两者之间存在细微差别,当想进行转换时需要慎之又慎!

循环结构:for#

for (initial-action; loop-continuation-condition; action-after-each-iteration){
   // loop body;
   Statement(s);
}

示例:

int i;
for (i = 0; i < 100; i++){
  cout << "Welcome to C++!\n";
}

上述循环执行100次。

一般来说,上述for语句可以有更简洁的写法:for (int i = 0; i < 100; i++) 这样写可以使得代码更加简略,但是极少数编译器不支持这种写法。

for循环的执行原理#

flowchart TD A[Initial-Action] --> B{Loop Continuation Condition?} B -- true --> C[Loop body] C --> D[Action-After-Each-Iteration] D --> B B -- false --> E[exit]

代码调试和综合练习(省略)#


一维数组#

数组(Array)#

  • 定义:数组是一种数据结构,用于表示一组相同类型的数据集合。
  • 声明:datatype arrayRefVar[arraySize];
  • 示例:
double myList[10];

c++要求任何数组声明的长度都必须是一个常量表达式。例如,下面的代码是不合法的;

int size = 4;
double myList[size]; // Wrong

但是下面的代码合法:

const int size = 4;
double myList[size]; // Correct
Warning

注:某些编译器支持可变长度数组(VLAs)作为扩展功能,但它们不属于C++标准的一部分。

int n;
cin >> n;
double arr[n]; // 某些编译器能通过,但这不是标准 C++!

访问数组元素#

数组元素通过下标进行访问,从零开始编号。
也就是说:假设数组a含有10个元素,则a[0] 代表a的第一个元素,a[9] 代表a的最后一个(也就是第10个)元素。
也就是说:长度为n的数组下标从0开始,到n-1结束。

Caution

c++没有数组边界检测(类似python的IndexError)!访问数组边界以外的元素不会造成语法错误,也不会在编译时被检测出来(也就是说能顺利通过编译),但运行时可能出现意想不到的结果!

Tip

如果你的代码运行时出现了Segmentation Fault,有很大概率就是数组越界了。

数组的初始化#

当一个数组被创建(还未赋初始值)时,它的元素可能为任意值(垃圾值)。

可通过以下代码在创建数组的同时直接赋值:

dataType arrayName[arraySize] = {value0, value1, ..., valuek};

示例:

double myList[4] = {1.9, 2.9, 3.4, 3.5}; // 与下列代码等价
double myList[4];
myList[0] = 1.9;
myList[1] = 2.9;
myList[2] = 3.4;
myList[3] = 3.5;

但是使用大括号初始化时,声明和赋值必须位于同一行。例如,下面的代码是错误的;

double mylist[4];
mylist = {1.9,2.9,3.4,3,5};

隐式指定数组大小(Implicit Size)#

c++允许你通过直接给数组中各个元素赋值的方式隐式声明和创建数组,例如下面的代码:

double myList[] = {1.9, 2.9, 3.4, 3.5};

c++编译器将会自动判断数组的元素并分配内存空间。

部分初始化#

c++允许你对数组的一部分元素进行初始化。在这种情况下,未被初始化的其他元素将会被自动置零。

double myList[4] = {1.9, 2.9}; //剩下两个元素是0,0

数组示例代码#

// Initializing Arrays with input value
const int ARRAY_SIZE = 10;
double myList[ARRAY_SIZE];
cout << "Enter" << ARRAY_SIZE << " values:";
for (int i = 0; i<ARRAY_SIZE;i++){
cin >> myList[i];
}
// Printing arrays
for (int i = 0; i < ARRAY_SIZE; i++){
      cout << myList[i] << " ";
}
// Copying Arrays
for (int i = 0; i < ARRAY_SIZE; i++){
   list[i] = myList[i];
}
Caution

c++中,不能直接将一个数组赋给另一个数组!形如list = myList; 的代码是错误的!

// Summing All Elements
double total = 0;
for (int i = 0; i < ARRAY_SIZE; i++){
    total += myList[i];
}
// Finding the Largest Element
double max = myList[0];
for (int i = 1; i < ARRAY_SIZE; i++){
    if (myList[i] > max)  max = myList[i];
}
// Finding the smallest index of the largest element
double max = myList[0];
int indexOfMax = 0;
for (int i = 1; i < ARRAY_SIZE; i++){
  if (myList[i] > max){
max = myList[i];
    indexOfMax = i;
  }
}

多维数组#

二维数组:数组的初始化#

elementType arrayName[rowSize][columnSize]; //注意是行在前,列在后

例如:

int matrix[5][5];

如果你想要像一维数组那样隐式指定数组大小,那么只需像下面这样:

int array[][3] = { {1,2,3}, {4,5,6} }; // 多维数组必须从右往左保证后续维度已知

当然也可以:

二维数组:示例代码#

// Printing Arrays
for (int row = 0; row < rowSize; row++){
  for (int column = 0; column < columnSize; column++){
    cout << matrix[row][column] << " ";
  }
  cout << endl;
}
// Summing Elements by Column
for (int row = 0; row < rowSize; row++){
  for (int column = 0; column < columnSize; column++){
    cout << matrix[row][column] << " ";
  }
  cout << endl;
}

多维数组#

以此类推。

double scores[10][5][2];
int a[][2][3] = {
    { {1,2,3}, {4,5,6} },
    { {7,8,9}, {10,11,12} }
// Right
int a[2][][3] = { { {1,2,3}, {4,5,6} } };  // Wrong
int a[][][3= { ... };                   // Wrong

c++字符串#

数字类型不能代表文字,所以我们需要一种特殊的数据类型处理一串连续字符,这种数据类型就是字符串(string)。

使用c++字符串需要调用<iostream> 头文件。

字符串可以通过以下几种方式初始化:

string s1 = "Hello";       // direct initialization
string s2("World");        // constructor style
string s3;                 // empty string
s3 = "C++";                // assignment after declaration
#include <iostream>
#include <string>
using namespace std;
int main() {
    string greeting = "Welcome to C++!";
    cout << greeting << endl;
    return 0;
}

与其他类型相似,我们可以使用cin来读取一串或多串字符串。
在此之外,如果读取的数据中有空格,我们也可以通过getline 函数读取一整行。

string firstName;
cin >> firstName;
string sentence;
getline(cin, sentence);

cin 仅读取一个单词。当遇到空格、制表符、换行符时,将停止读取。
getline 读取一整行,包括空格。

Important

如果cin读取的单词位于一行的末尾,则结尾的换行符\n将仍然留在输入缓冲区中,需要使用cin.ignore()清除。

cout << "Enter your age: ";
cin >> age;
cin.ignore();        //discard the leftover newline
cout << "Enter your name: ";
getline(cin, name);

字符串的常用运算#

操作示例代码说明
拼接s3 = s1 + s2;合并两个字符串
追加s1 += "!";在末尾追加内容
长度s1.length()s1.size()返回字符数
清空s1.clear()变为空字符串
判断空s1.empty()返回 truefalse

字符串比较#

c++允许使用和数字类型相同的运算符(==,!=,<,<=,>,>=)直接对字符串进行比较。

string a = "apple";
string b = "banana";
if (a < b)
    cout << a << " comes before " << b << endl;

你也可以使用string.comparison() 成员函数进行比较。

string s1 = "Hello";
string s2 = "Hi";
int result = s1.compare(s2);

注意运算符返回bool类型,而成员函数返回int类型,含义如下。

返回值含义
0字符串相等
< 0s1 小于 s2(按字典顺序)
> 0s1 大于 s2(按字典顺序)

大小比较规则:
1.从前往后,比较每个字符的ASCII码。
2.如果前缀相同(即比较过的每个字符都相同,但两个字符串长度不同,如cat和cats),更长的字符串更大。
3.完全相等时,字符串相等。

子串和搜索#

子串(substr)#

substr() 函数用来截取字符串的一部分。

// 形式1:只指定起始位置,截取从该位置到字符串末尾的子串
string substr(size_t pos) const;
// 形式2:指定起始位置和子串长度,截取固定长度的子串
string substr(size_t pos, size_t count) const;
string s = "programming";
cout << s.substr(0, 3) << endl;   // "pro"
cout << s.substr(3, 4) << endl;   // "gram"
cout << s.substr(7) << endl;      // "ming"

substr() 如果提供两个参数,则分别为起始位置与截取长度而非起始位置与终止位置

搜索(find)#

find()函数用来查找字符或子串,返回首个符合要求的元素的下标。

size_t find(const string& str, size_t pos = 0) const;
string s = "Welcome to C++ programming";
cout << s.find("come") << endl;   // 3
cout << s.find('o') << endl;      // 4
cout << s.find("Java") << endl;   // string::npos (not found)
cout << s.find("o", 5)<< endl;   // search from index 5 onward
string email = "alice@tju.edu.cn";
int pos = email.find('@');
string user = email.substr(0, pos);
cout << "Username: " << user;

运算符总结#

OperatorDescription
[]使用数组下标运算符访问字符
=将一个字符串的内容复制到另一个字符串
+将两个字符串连接成一个新字符串
+=将一个字符串的内容追加到另一个字符串
<<将字符串插入到流中
>>从流中提取字符到字符串,以空白字符或空终止符为分隔
==, !=, <, <=, >, >=用于比较字符串的六个关系运算符
Important

字符串部分有很多比较常用的函数,这里仅介绍了substr和find.建议自行查找学习更多。


指针#

定义与声明#

指针是将内存地址作为存储值的变量。

指针是一种复合数据类型。

声明方式如下:

dataType * pointerName;

例如:

int * pCount;
Tip

*号前面或后面的空格是可选的。c++程序员倾向于使用int* p,而由c过渡而来的程序员倾向于使用int *p.

隐式初始化#

若未在声明指针时直接赋予地址(而是在后期执行),则c++编译器默认会按照以下流程分配地址:

  • 如果是本地指针(local pointer),则将指向随机地址;
  • 如果是全局指针(global pointer),则将指向NULL(pointing nothing).
  • 此处的NULL在内部被定义为0.
Caution

永远不要在未赋予地址的情况下,使用上述未经过完整初始化(声明+赋值)的指针!这样做导致的后果可能比使用未经过初始化的变量更为严重(RuntimeError或者意外内存数据更改)!

显式初始化(推荐)#

即在声明指针同时赋予地址。

int count=5;
int *pCount = &count; // & 是取地址运算符:&count代表变量count对应的内存地址
int *ptr = NULL;

指针的作用:间接引用(Indirect Reference)#

通过指针引用变量。

例如:

int count=5;
int *pCount = &count;
count++; //direct reference
(*pCount)++; //indirect reference

指针和变量的等价关系如下:

int age;
int *age_ptr = &age;
age_ptr <=> &age
*age_ptr <=> age
*age_ptr = 50 <=> age = 50;
(*age_ptr)++ <=> age++;

Null 指针(空指针)#

null 指针是任何指针类型的常规指针,它具有一个特殊值,指示它不指向任何有效的引用或内存地址。此值是将整数值零类型转换为任何指针类型的结果。

int * p;
p = 0; // p has a null pointer value
p = NULL; or p = nullptr;//C++ 11

指针的运算#

对指针的运算操作的是指针对应的地址,而非指针指向的值。

指针具有以下运算:

  • 分配(Assignment)
  • 移动(Movement)
  • 减法(Subtraction)
  • 比较(Comparison)

指针的类型决定了运算的结果。例如:下面的加/减运算代表指针向前/向后移动n个指针类型对应的类型长度, 而同类型指针相减得到的值是他们之间间隔的该种元素的个数。

两地址相减结果为p指针与a数组首地址之间元素的个数。

p±n;// move n*sizeof( pionter type)

指针的比较#

关系运算符(<,>,=等) 可以用来比较指针的地址。

比较指针的地址和比较指针的值是两个完全不同的概念。

  if (ptr1 == ptr2)  ... // compares addresses
  if (*ptr1 == *ptr2) ... // compares contents

一些常见的注意事项#

  • 对指针分配的地址指向的数据类型必须和指针本身的数据类型保持一致。
int area = 1;
double *pArea = &area; // Wrong!
  • 每一个*对应一个指针。
int *ptr1, *ptr2; //two pointers
int* ptr1, ptr2; // one pointer, one integer

特殊而又容易混淆的概念:指针常量和常量指针#

指针常量(Constant Pointer)#

一个地址无法被改变的指针。指针指向的地址固定,但指向的值可以改变。

double radius = 5;
double another = 10;
double * const pValue = &radius;
(*pValue) = 3.0; // Right
raidus = 4.0 // Right
pValue = &another; // Wrong
pValue ++; // Wrong

常量指针(Pointer of Constant)#

一个值无法被改变(即指向常量)的指针。指针指向的值固定,但地址可以改变。

double radius = 5;
double another = 10;
const double * pValue = &radius;
(*pValue) = 3.0; // Wrong
raidus = 3.0 // Right ()
pValue = &another; // Right:现在 pValue 指向 another

pValue是一个指向常量的指针,即不能通过pValue来修改它所指向的值。但是,radius本身并不是常量,所以可以直接修改radius的值。

Tip

常量指针承诺不修改它所指向的值,但不保证所指向的值本身是常量。

指针与数组名的关系#

数组名代表数组起始位置元素的地址,因此也就能和指针顺理成章扯上联系。

int myList[10]; // myList <=> &myList[0]

数组变量本质上是与指针非常类似。
也就是说:你可以通过myList+1这种形式对数据进行操作。

然而,他们在本质上还是存在不同:数组名是一个常量(而非变量)地址。

int array[10];
int* ptr = array;
for (k=0; k<5; k++){
  cout<< *(ptr++); // Right
}
for (k=0; k<5; k++){
  cout<< *(array++); // Wrong
}

指针和数组名相减:两地址相减结果为p指针与a数组首地址之间元素的个数。

一些比较恶心的考点#

#include <iostream>
using namespace std;
int main(){
    int x[4] = {10,20,30,40};
    int *p = x; // p指向x[0]
    cout<<*++p<<endl;
    cout<<*p++<<endl;
    cout<<++*p<<endl;
    cout<<(*p)++<<endl;
    cout<<*p<<endl;
    return 0;
}

不会了?回去看看

多维数组与指针(暂略)#

使用指针发访问二维数组(暂略)#


字符数组#

定义与声明#

有别于普遍使用的string,为了向下兼容c,c++中也允许使用字符数组(a.k.a c风格字符串(C-strings) or 基于指针的字符串(pointer-based strings))来表示一串连续的字符。

有两种方法声明c风格字符串:

// 1.Using an array variable
char city[7] =  "Dallas";
char city[] =  {'D', 'a', 'l', 'l', 'a', 's', '\0' };
char city[] =  "Dallas";
cout << city;
//2.Declare a string variable using a pointer
const char *pCity = "Dallas"; //不能通过 pCity 修改它指向的字符串常量(存储在常量区)
cout<<pCity;
Important

C风格字符串的有效字符长度是数组长度-1,因为需要预留一个位置给\0。切记切记!

输入:cin与cin.getline()#

Caution

此处的cin.getline()与上文介绍字符串时的getline()不同,切勿混淆!

特性std::getline (全局函数)cin.getline() (成员函数)
头文件<string><iostream>
类型全局函数istream类的成员函数
目标类型std::stringchar[] (C风格字符串)
安全性更安全,自动管理内存需要指定缓冲区大小,可能溢出
现代性现代C++推荐使用更传统,兼容C风格
char city[10];
cin >>city; // Right; but only a word.stop at the first whitespace.
char *city2;
cin>>city2; //Wrong; No memory is allocated to store the content of city2.
cin.getline(char array[], int size, char delimitChar) // can read any character.
cin.getline(city,5,'\n') // example;

输出:cout#

cout << "Enter a city: ";
cin >> city; // read to array city
cout << "You entered " << city << endl

和吃饭喝水一样简单。

字符测试函数(Character Test Functions)与大小写转换函数(Case Conversion Functions)#

Important

以下函数所作用的对象都是单个char字符。

  • 当需要对字符数组操作时,考虑for循环操作;
  • 当需要对string操作时,可先用c_str()方法将其转换为字符数组,也可使用for循环操作单个字符。
  • 这些函数位于<cctype>头文件中,使用时记得先声明。
  • 以下内容并非全部要求记忆。
FunctionDescriptionExample
isdigit(c)Returns true if c is a digit.isdigit('7') is true isdigit('a') is false
isalpha(c)Returns true if c is a letter.isalpha('7') is false isalpha('a') is true
isalnum(c)Returns true if c is a letter or a digit.isalnum('7') is true isalnum('a') is true
islower(c)Returns true if c is a lowercase letter.islower('7') is false islower('a') is true
isupper(c)Returns true if c is an uppercase letter.isupper('a') is false isupper('A') is true
isspace(c)Returns true if c is a whitespace character.isspace('\t') is true isspace('A') is false
isprint(c)Returns true if c is a printable character including space.isprint(' ') is true isprint('A') is true
isgraph(c)Returns true if c is a printable character excluding space.isgraph(' ') is false isgraph('A') is true
ispunct(c)Returns true if c is a printable character other than a digit, letter, or space.ispunct('*') is true ispunct(',') is true ispunct('A') is false
iscntrl(c)Returns true if c is a control character such as '\n', '\f', '\v', '\a', and '\b'.iscntrl('*') is false iscntrl('\n') is true iscntrl('\f') is true
tolower(c)Returns the lowercase equivalent of c, if c is an uppercase letter. Otherwise, return c itself.tolower('A') returns 'a' tolower('a') returns 'a' tolower('\t') returns '\t'
toupper(c)Returns the uppercase equivalent of c, if c is a lowercase letter. Otherwise, return c itself.toupper('A') returns 'A' toupper('a') returns 'A' toupper('\t') returns '\t'

字符数组函数#

  • str开头的函数位于<cstring>头文件中,含有to 的函数位于cstdlib头文件中,使用时记得先声明。
  • 以下内容并非全部要求记忆。
FunctionDescription
int strlen(char *s1)Returns the length of the string, i.e., the number of the characters before the null terminator.
char *strcpy(char *s1, const char *s2)Copies the string s2 to string s1. The value in s1 is returned.
char *strncpy(char *s1, const char *s2, size_t n)Copies at most n characters from string s2 to string s1. The value in s1 is returned.
char *strcat(char *s1, const char *s2)Appends string s2 to s1. The first character of s2 overwrites the null terminator in s1. The value in s1 is returned.
char *strncat(char *s1, const char *s2, size_t n)Appends at most n characters from string s2 to s1. The first character of s2 overwrites the null terminator in s1 and appends a null terminator to the result. The value in s1 is returned.
int strcmp(const char *s1, const char *s2)Returns a value greater than 0, 0, or less than 0 if s1 is greater than, equal to, or less than s2 based on the numeric code of the characters.
int strncmp(const char *s1, const char *s2, size_t n)Returns a value greater than 0, 0, or less than 0 if the n characters in s1 is greater than, equal to, or less than the first n characters in s2 based on the numeric code of the characters.
int atoi(const char *s1)Converts the string to an int value.
double atof(const char *s1)Converts the string to a double value.
long atol(const char *s1)Converts the string to a long value.
void itoa(int value, char *s1, int radix)Converts the value to a string based on specified radix.

函数#

为什么我们需要函数?#

  • 避免代码重复(Avoid code duplication)
  • 使程序模块化(Make programs modular)
  • 增强可读性与可维护性(Improve readability and maintenance)
  • 将复杂的问题分解(Break complex problems into smaller tasks)

定义与声明#

函数是一段执行特定任务的代码块。

一个函数由四个部分组成:

  • 返回值类型(Return value type)
  • 函数名(Function name)
  • 参数(Parameter)
  • 函数体(Function body)

声明格式如下:

returnValueType functionName(list of parameters){
    //function body;
}
  • 函数签名(Function signature) 是由函数名(functionName) 和参数列表(list of parameters) 组合而成的。
  • 函数头中定义的变量称为形式参数 (formal parameters) 或形参。
  • 当调用函数时,你向参数传递一个值。该值被称为实际参数 (actual parameter) 或实参 (argument)。
  • 函数可以返回一个值。returnValueType 表示函数返回值的数据类型。如果函数不返回值,则 returnValueType 为关键字 void。例如,main 函数中的 returnValueType 为 int。

内存空间与函数的栈调用(Call Stacks)#

一些容易误解的地方:值传递#

当你调用带参数的函数时,参数的值会被传递给该函数。这被称为值传递(pass-by-value)。如果参数是变量而非字面值,则该变量的值会被传递给参数。无论函数内部对参数做了何种修改,该变量本身都不会受到影响


函数补充#

函数重载(Overloading Functions)#

重载函数允许你定义具有相同名称的函数,只要它们的签名不同即可。

#include <iostream>
using namespace std;
// 返回两个整数中的较大值
int max(int num1, int num2){
return (num1 > num2) ? num1 : num2;
}
// 返回两个双精度浮点数中的较大值
double max(double num1, double num2){
return (num1 > num2) ? num1 : num2;
}
// 返回三个双精度浮点数中的最大值
double max(double num1, double num2, double num3){
return max(max(num1, num2), num3);
}
int main(){
// 测试
cout << "max(3, 5) = " << max(3, 5) << endl;
cout << "max(2.5, 3.7, 1.2) = " << max(2.5, 3.7, 1.2) << endl;
return 0;
}

此处的三个max函数虽然名字相同,但通过参数个数和类型的区分实现了函数重载。

Important

注意:返回类型不同不能作为函数重载的依据!

模糊调用(Ambiguous Invocation)#

以下代码无法通过编译:

#include <iostream>
using namespace std;
int maxNumber(int num1, double num2){
  if (num1 > num2)
    return num1;
  else
    return num2;
}
double maxNumber(double num1, int num2){
  if (num1 > num2)
    return num1;
  else
    return num2;
}
int main(){
  cout << maxNumber(1, 2) << endl; // 1也可被自动转换为double,导致编译器不知道应该调用哪个maxNumber函数
  return 0;
}

函数原型(Function Prototypes)#

在调用函数之前,必须先声明该函数。确保声明的一种方法是将声明置于所有函数调用之前。另一种方法是在调用函数之前声明函数原型。函数原型是指不包含实现的函数声明,其实现可在程序后续部分给出。

#include <iostream>
using namespace std;
int max(int num1, int num2); //
double max(double num1, double num2);
double max(double num1, double num2, double num3);
int main(){
// 测试
cout << "max(3, 5) = " << max(3, 5) << endl;
cout << "max(2.5, 3.7, 1.2) = " << max(2.5, 3.7, 1.2) << endl;
return 0;
}
// 在main之后声明
int max(int num1, int num2){
return (num1 > num2) ? num1 : num2;
}
double max(double num1, double num2){
return (num1 > num2) ? num1 : num2;
}
double max(double num1, double num2, double num3){
return max(max(num1, num2), num3);
}

默认参数(Default Arguments)#

c++允许声明有默认值的函数。如果函数被调用时没有被传递相应参数,默认值将会被传递。

int max(int num1 = 1, int num2 = 2){
return (num1 > num2) ? num1 : num2;
}
max(2,1) // 传递,正常运算
max() // 未传递,使用默认值

当函数包含带默认值和不带默认值的参数时,带默认值的参数必须最后声明。

变量的作用域(Scope of Variables)#

局部变量:在函数内部定义的变量。
作用域:程序中可以引用相应变量的部分。

变量的作用域从其声明处开始,持续到包含该变量的代码块结束。局部变量必须在使用前声明。

在函数的不同代码块中,可以多次声明同名的局部变量,但不能在同一个代码块中重复声明同一个局部变量。(这样做不是好的代码风格,可能会导致混淆,建议尽量避免)

全局变量(Global Variables)#

C++ 还允许使用全局变量。它们在所有函数(包括main)外部声明,其作用域内的所有函数均可访问。局部变量没有默认值,而全局变量默认值为零。

作用域解析运算符(Unary Scope Resolution)(::)#

如果局部变量名与全局变量名相同,可通过 ::globalVariable 访问全局变量。 :: 运算符称为一元作用域解析运算符.

#include <iostream>
using namespace std;
int v1 = 10;
int main(){
  int v1 = 5;
  cout << "local variable v1 is " << v1 << endl; // 5
  cout << "global variable v1 is " << ::v1 << endl; // 10
  return 0;
}

for循环中局部变量的作用域(Scope of Local Variables in a for loop)#

在 for 循环头部的初始操作部分声明的变量,其作用域覆盖整个循环。但在 for 循环体内部声明的变量,其作用域仅限于循环体内部,从声明处起至包含该变量的代码块结束为止。

Warning

极其不建议在嵌套循环中使用同名局部变量。

静态局部变量(Static Local Variables)#

C++允许声明静态局部变量。静态局部变量只在第一次调用时初始化一次,会在内存中永久分配,其生存周期与程序生命周期相同。声明静态变量时需使用关键字static

#include <iostream>
using namespace std;
void t1(); // function prototype
int main()
{
t1(); // 2,2
t1(); // 3,2
return 0;
}
void t1()
{
static int x = 1; 注意此处的static;
int y = 1;
x++;
y++;
cout << "x is " << x << endl;
cout << "y is " << y << endl;
}

引用变量(Reference Variables)与引用传递#

引用变量是其他变量的别名。

为了声明一个引用变量,我们使用取地址运算符&

int n=5;
int &r=n;

以下是一个示例代码:

#include <iostream>
using namespace std;
int main() {
int i = 5;
int& r = i; // r 是 i 的引用
cout << "Value of i : " << i << endl; // 5
cout << "Value of i reference : " << r << endl; // 5
return 0;
}

引用与指针的区别#

引用与指针有三个主要区别:

  1. 不存在空引用:引用必须连接到一块合法的内存。
  2. 不可重新绑定:一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  3. 必须初始化:引用必须在创建时被初始化。指针可以在任何时间被初始化。

通过引用传递函数参数的好处#

使用引用作为函数参数,可以避免拷贝大对象,提高程序的运行速度。例如,交换两个变量的值:

#include <iostream>
using namespace std;
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
cout << "Before swap: x = " << x << ", y = " << y << endl;
swap(x, y);
cout << "After swap: x = " << x << ", y = " << y << endl;
return 0;
}

指针补充#

空指针(Void Pointer)#

空型指针是一种特殊类型的指针。在C++中,void 表示缺少类型,因此 void 指针是指向没有类型的值的指针(因此也是不确定的长度和不确定的取消引用属性)。

这允许 void 指针指向任何数据类型,从整数值或浮点数到字符串。

但是作为交换,它们有很大的局限性:它们指向的数据不能直接取消引用(这是合乎逻辑的,因为我们没有要取消引用的类型),因此,在取消引用之前,我们始终必须将 void 指针中的地址强制转换为指向具体数据类型的其他指针类型。

#include <iostream>
using namespace std;
void increase (void* data, int psize) {
  if ( psize == sizeof(char) ) {
  char* pchar;
  pchar=(char*)data; // 强制转换为char*
  ++(*pchar);
  }
  else if (psize == sizeof(int) ) {
  int* pint;
  pint=(int*)data; // 强制转换为int*
  ++(*pint);
  }
}
int main () {
char a = 'x';
int b = 1602;
increase (&a,sizeof(a)); increase (&b,sizeof(b));
cout << a << ", " << b << endl;
return 0;
}

函数中的指针#

指针作为参数(Pointer as parameter)与指针传递#

在这种情况下,指针所指向的地址将被传递给函数。部分资料认为这也是一类按引用传递(pass-by-reference),实则不然。

这里的”部分资料”指的就是学校的ppt. 好误导啊。

通过指针传递参数,可以实现类似引用传递的效果,但其本身的机制仍然是值传递。

void swap(int *p1, int *p2){ //交换了p1和p2所对应的值
int temp;
  temp = *p1;
  *p1   = *p2;
  *p2   = temp;
}
void swap(int *p1, int *p2){ // 交换了指针变量p1和p2本身,
int *temp;  //但这仅仅是对形参的交换,不影响实参,因此无法实现函数功能
  temp = p1;
  p1 = p2;
  p2 = temp;
}

指针作为返回值(Pointer as Return Value)#

int max=0;
int *getMax();
void main(){
  int *p;
  p=getMax();
  cout<<"Max is "<<(*p);
}
int *getMax(){
  int tmp=max, i;
  for(i=0;i<3;i++){
  cout<<"Please enter No. "<<i<<endl;
  cin>>tmp;
  if(tmp>max) max=tmp;
  }
  return &max;
}
Warning

不要返回局部指针!函数返回值后内存被清理,该指针无效!

将数组传递给函数#

double getAverage(int arr[], int size);
int main ()
{
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;
   cout << balance << endl;
   cout << &balance << endl;
   cout << &balance[1]<< endl;
   avg = getAverage( balance, 5 ) ;
   cout << "平均值是:" << avg << endl;
   return 0;
}
double getAverage(int arr[], int size){
  int    i, sum = 0double avg;
  for (i = 0; i < size; ++i) sum += arr[i];
  avg = double(sum) / size;
  cout << arr << endl;
  cout << &arr << endl;
  cout << &arr[0] << endl;
  cout << &arr[1] << endl;
  return avg;
}

动态内存分配(Dynamic Memory Allocation)#

这种方式允许程序员**动态、“手动”**地分配空间。

  • 动态:在代码执行时才分配,而非编译时
  • 手动:使用显式语句

new 关键字:分配内存#

int *pValue = new int;
int *mylist = new int[10]; // 动态数组

delete 关键字:释放内存#

通过 new 分配的内存将一直保持可用,除非你显式释放(通过 delete) 或者程序运行终止。

delete pValue;
delete [] mylist;
Caution

至此,你已经掌握了动态空间中最重要、也是最危险的魔法。记住:每一个使用 new 分配的变量一定要确保被 delete 掉(对于动态数组,使用delete [])。如果在尚未释放时就将指针转向别处(例如下面的代码),将会导致最大的麻烦:内存泄漏!

int *p = new int;
// 此处缺少了delete p;
p = new int; // 最上面一行分配的int空间将再也无法被调用,也无法被清除!

多维动态数组#

以二维4x4动态数组为例:

typedef int* IntArrayPtr;
IntArrayPtr *m = new IntArrayPtr[4];
for (int i = 0; i<4; i++)  m[i] = new int[4]; // 一般格式
int **m;
m=new int *[4];
for(int i=0;i<4;i++){
   m[i]= new int[4]; // 示例
}

系统总结:值传递、引用传递、指针传递#

  • 值传递:将实际参数的一个副本传递给函数。在函数内部修改这个副本,不会影响原始数据。

  • 引用传递:将实际参数的引用(别名、内存地址的绑定) 本身传递给函数。在函数内部通过这个引用进行操作,直接影响原始数据。

  • 指针传递:传递的是一个指针变量的值(即一个内存地址)。这个传递过程本身是值传递(传递了地址值的副本),但因为通过这个地址可以找到并操作原始数据,所以达到了类似引用传递的效果。


递归(Recursion)#

定义#

递归:一个函数调用它自身的行为。

对于较大的问题,通常可以通过递归将其化简为重复性的解决小的问题。(例如Fibonacci数列,阶乘,汉诺塔,二分…)

为什么我们需要递归?#

  • 递归能以简单自然的方式表达复杂问题。
  • 特别适用于树结构、分治法和数学定义。

典型的递归流程#

树状递归解决阶乘问题#

递归解决Fibonacci数列#

递归的本质特征#

所有递归方法都具有以下特征:

  • 使用一个或多个基准情况(最简单的情况)来终止递归。
  • 每次递归调用都会缩小原始问题规模,使其逐步接近基准情况直至完全符合该情况。

通常,使用递归解决问题时需将其分解为子问题。若子问题与原始问题相似,则可采用相同方法递归求解该子问题。此类子问题本质上与原始问题几乎相同,只是规模更小。

迭代(Iteration)与递归的区别#

比较方面迭代 (Iteration)递归 (Recursion)
控制结构使用重复结构(如forwhile循环)使用选择结构(如if-else条件判断)
重复机制显式使用重复结构直接控制重复过程通过函数自身的重复调用实现重复
终止条件当循环继续条件失败时终止当基本情况(base case)被识别时终止
终止过程修改循环变量直到循环条件不满足不断产生更简单版本的问题直到基本情况
无限情况无限循环:循环继续条件始终为真无限递归:递归步骤无法收敛到基本情况
内存使用通常占用固定内存空间(循环变量)每次调用创建新的栈帧,可能消耗更多栈空间
代码思维命令式,关注如何重复操作声明式,关注问题如何分解
典型应用数组遍历、计数循环、简单重复任务树/图遍历、分治算法、递归定义的问题

补充:欧几里得算法(辗转相除法)#

int gcd(int a,int b){
return b == 0 ? a : gcd( b, a %b )
}

结构体(struct)#

定义与声明#

结构体也是一种数据类型,其中每个值都是由组成项构成的集合。

  • 整个集合拥有统一名称;
  • 每个组成项可被单独访问;
  • 可将不同类型的相关数据打包整合,以便于在同一个标识符下快捷访问。

一个标准的示例:

struct  AnimalType // Declares a  struct data type
{  //  does not allocate memory
    long     id;
    string   name;
    string   genus;
    string   species;
    string   country;
    int      age;
    float    weight;
    string   health;
};
// Declare  variables of AnimalType
AnimalType thisAnimal;
AnimalType anotherAnimal;

结构体声明定义了类型并命名了结构体的成员.

Important

结构体不会为该类型的任何变量分配内存。你仍需单独声明结构体变量。

结构体的作用域#

若结构体类型声明位于所有函数之前,则在文件其余部分均可见 ;
若声明置于函数内部,则仅该函数可使用该结构体.

这一点和局部变量以及全局变量比较类似。

通常将结构体类型声明置于(.h)头文件中,并通过#include包含该文件。

不同结构体成员可能拥有相同标识符;非结构体变量也可能与结构体成员使用相同标识符。

访问结构体元素#

结构体访问需要使用成员选择运算符 .

在结构体类型声明之后,只有在结构体变量名前加上点号,才能在程序中使用各个成员,例如: thisAnimal.weight, anotherAnimal.country.

结构体成员的类型决定了可以对其执行的运算。

对结构体类型变量有效的操作包括:

  • 赋值给同类型的另一个结构体变量
  • 作为参数传递(按值或按引用)
  • 作为函数返回值
anotherAnimal = thisAnimal;       // Assignment
WriteOut(thisAnimal);      // Value parameter
ChangeWeightAndAge(thisAnimal);  // Reference parameter
thisAnimal = GetAnimalData();    // Function return value

禁止对整个结构体变量进行输入输出、算术运算及比较操作!

通过引用传递结构体#

void ChangeAge(AnimalType& thisAnimal){
    thisAnimal.age++;
}

返回结构体类型#

AnimalType GetAnimalData ()
{
    AnimalType  thisAnimal;
    char response;
    do {
    // Have user enter members until they are correct
...
    while (response != ‘Y’);
    return  thisAnimal;
}

分层结构体/嵌套结构体(Hierarchical / Nested Structures)#

当一个结构体内部包含另外一个结构体类型时,称为嵌套/分层结构体。
当在每个结构体中有大量详细信息时,这种结构会非常有用。
下面是一个三层结构体的嵌套:

struct  DateType{
    int    month;           //  Assume 1 . . 12
    int    day;  //  Assume  1 . . 31
    int    year;   //  Assume 1900 . . 2050
};
struct  StatisticsType{
    float  failRate;
    DateType  lastServiced;  //  DateType is a struct type
    int downDays;
};
struct MachineRec{
    int idNumber;
    string description;
    StatisticsType  history; // StatisticsType is a struct
    DateType  purchaseDate;
    float cost;
};
MachineRec  machine;

调用多层嵌套,只需像下面这样:

machine.history.lastServiced.year = 2025;

联合体与抽象数据类型#

联合体定义及声明#

联合体是一种在程序运行期间同一时间内仅存储其成员中的一种的结构。

声明示例如下:

union  WeightType{  // Declares a union type
    long  wtInOunces;
    int   wtInPounds;
    float wtInTons;
};
WeightType weight; // Declares a union variable
weight.wtInTons = 4.83;
//  Weight in tons is no longer needed
//  Reuse the memory space
weight.wtInPounds = 35;

抽象(Abstraction)#

简单来说,抽象就是将对象的本质特征与其运作或构成细节分离的过程。

举个例子,你只需要知道牛吃草,但不需要知道牛是怎么消化的,更不需要知道它的反刍细节。

抽象聚焦于‘是什么’,而非‘怎么做’。

抽象对于处理大型、复杂的软件项目具有奇效。

控制抽象 (Control Abstraction)#

控制抽象将动作的逻辑属性与其实现分离。

函数调用依赖于函数的规范(描述),而非其实现(算法)。

数据抽象(Data Abstraction)#

以此类推,数据抽象将数据的逻辑属性与其实现分离。

数据抽象实现
What are the possible values? How can this be done in C++?
What operations will be needed? How can data types be used?

再举个例子 ,用抽象的角度打开 int 类型:

  • int类型具有定义域:-2147483648 ~ 2147483647;
  • int类型支持下列运算:+ - * / % >> <<

你无需关注实现细节。

抽象数据类型(Abstract Data Type)#

抽象数据类型是一种数据类型,其属性(域和操作)的定义(是什么)独立于任何具体实现(如何实现)。

再一次举个例子:

上面的一切介绍都在为面向对象以及接下来要介绍的类打下基础。


类(class)#

一些没啥用也很难懂的前置知识(C++中类的基本使用哲学和术语)#

  • C++的类机制促进了抽象数据类型(ADT)代码的重用
  • 使用该类的软件称为客户端(Client)
  • 该类型的变量称为类对象或类实例
  • 客户端代码通过类的公共成员函数操作类对象 (这是封装原则的直接体现)

是不是很抽象?抽象就对了,因为我也看不懂

引入:类的使用示例#

#include   “time.h   // Includes specification of the class
using namespace std;
int main(){
    Time currentTime; // 声明两个Time类 (头文件中已经定义好)
    Time endTime;
    bool done = false;
    currentTime.Set(5, 30, 0);
    endTime.Set(18, 30, 0);
    while (!done) {
    ...
        currentTime.Increment ();
        if  (currentTime.Equal (endTime)) done = true;
    };
}

类的声明与结构#

类声明创建数据类型并命名类成员。

一个类由两种成员构成:数据成员和函数成员。

  • 数据成员:也被称为“成员变量”或“属性”。它们用于存储对象的状态信息。

  • 函数成员:也被称为“成员函数”或“方法”。它们是定义在类内部的函数,用于操作数据成员,定义对象的行为。

  • class的成员默认是private的

  • struct的成员默认是public的

  • 这是class和struct在C++中的主要区别之一 私有类成员只能被类成员函数访问,不能被客户端直接访问。

Tip

上面对公有/私有的解释并不完全。事实上,还有一种权限:受保护(protected.) 详见下文.#protected关键字与访问权限

它不会为该类型的任何变量分配内存!
客户端代码仍需声明类变量。

类的定义域空间#

每个函数成员都可以访问类主体块中任意顺序的其他成员(函数或数据成员)。
“先声明再调用”规则不适用于类成员。

类成员的访问#

适用于类对象的内置操作包括:

  • 使用点(.)运算符选择成员,
  • 使用(=)赋值给另一个类变量,
  • 作为参数传递给函数(按值或按引用传递),
  • 作为函数的返回值。

其他操作可定义为类成员函数。

成员选择运算符 (.) 可选择数据成员或函数成员. (参考下面的定义,这样调用可以体现这一运算符对不同成员的作用)

currentTime.Equal (endTime)
currentTime.hrs = 10

类的完整定义(分离式)#

// Specification file “time.h”
// Specifies the data and function members
class Time {
// 未手动指定权限类型(即没有放在public或者private内部)的成员,遵循默认规则。
public:
    void Set(int hours, int minutes, int seconds);
    void Increment();
    void Write()  const;
    bool Equal(Time otherTimeconst;
    bool LessThan(Time otherTimeconst;
private:
    int hrs;
    int mins;
    int secs;
}; //----------------------------不要忘记分号!!!!!
// Implementation file “time.cpp”
// Implements the Time member functions.
#includetime.h // Also must appear in client code
#include  <iostream>
bool Time::Equal(/* in */  Time otherTime) const {
// Postcondition:  Return value == true,
//     if this time equals otherTime,
//       otherwise == false
    return ((hrs == otherTime.hrs)
          && (mins == otherTime.mins)
          && (secs  == otherTime.secs));
}
...

经典回顾 - 我们之前用到了哪些类?#

头文件 iostream 和 fstream 声明了输入输出类 istream、ostream、ifstream 和 ofstream.

cin 和 cout 均为类对象,get 和 ignore 则是函数成员。

cin.get(someChar);
cin.ignore(100, ‘\n’);

以下语句声明 myInfile 为 ifstream 类的实例并调用成员函数 open。

ifstream myInfile;
myInfile.open(“mydata.dat”);

由类看向OOP - 封装#

类实现细节对客户端不可见。类的公共函数为客户端代码与类对象之间提供接口。

作用域在类中的作用#

C++程序通常使用多种类类型。不同类可能包含具有相同标识符的成员函数,例如Write().

成员选择运算符用于确定应用Write()成员函数的对象:

currentTime.Write(); // Time类
numberZ.Write(); // ComplexNumber类

在实现文件中,作用域解析运算符用于函数成员名称前的标题以指定其所属类。

void Time::Write () const{
...
}

阶段性总结#

上图是两个基于前文构造出的类。白色椭圆代表可与外界交互的成员函数,在类的内部运算与外界之间起到了桥梁的作用;而私有数据完全被框定在类内部,无法直接与外界交互。

上图是使用类的一个程序的完整架构。其中,上面的三个文件是我们自己写的。

类成员函数的const使用#

当成员函数不修改私有数据成员时,需在函数原型(位于规范文件中)和函数定义的标题(位于实现文件中)两处均使用 const 关键字。

避免多重引用 - 特殊处理#

通常多个程序文件会使用相同的头文件,其中包含类型别名声明、常量或类类型声明——但在同一命名空间内重复定义相同标识符会导致编译时错误。

因此,为了避免出现这样‘撞车’的惨案发生,我们可以使用下面的预处理指令:

// <example.h> 头文件
#ifndef   Preprocessor_Identifier
#define  Preprocessor_Identifier
...
#endif

例如,对上面的<time.h> 可以做如下修改:

#ifndef TIME_H
#define TIME_H
// Specification file “time.h”
// Specifies the data and function members
class Time {
int a,b;
int hrs;
    int mins;
    int secs;
int test();
public:
    bool Equal(Time otherTime);
    bool Set(int h,int m,int s);
    void Write(...);
    void increment(...);
    int LessThan(...);
private:
    int test2();
}; //----------------------------不要忘记分号!!!!!
#endif

类构造函数#

类构造函数是用于初始化类对象私有数据成员的成员函数.

  • 构造函数的名称始终与类名相同,且不具有返回类型
  • 一个类可包含多个具有不同参数列表的构造函数
  • 无参数构造函数即为默认构造函数

当声明类对象时构造函数会被隐式调用;若存在参数,其值需在声明中用括号括起。

class  Time{  // Time.h
public :
    void Set(int hours, int minutes, int seconds);
    void Increment();
    void Write()  const;
    bool Equal(Time otherTimeconst;
    bool LessThan(Time otherTimeconst;
    // Parameterized constructor
    Time (int initHrs, int initMins, int initSecs); // 构造函数!
    // Default constructor
    Time(); //也是构造函数,但是没有传入参数!(运用重写)
    //~Time(); // 未明确指定析构函数
private :  //  3 data members
    int hrs;
    int mins;
    int secs;
};
// Time.cpp 补充内容
Time::Time ()
// 默认构造函数
// 执行后:
//  hrs == 0 && mins == 0 &&  secs == 0
{
    hrs  = 0;
    mins = 0;
    secs = 0;
}
Time::Time(int initHrs,int initMins,int initSecs){
//  也是构造函数
//  执行前:
//      0 <= initHrs <= 23 && 0 <= initMins <= 59
//      0 <= initSecs <= 59
//  执行后:
//    hrs == initHrs && mins == initMins && secs == initSecs
    hrs  =  initHrs;
    mins =  initMins;
    secs =  initSecs;
}

不同的运行结果


继承与多态#

OOP的三大特性#

封装(Encapsulation)、继承(Inheritance)、多态(polymorphism)!

由类看向OOP - 继承 (基类和派生类)#

继承#

继承使您能够定义一个通用类(即基类),并随后将其扩展为更专业的类(即派生类)。这些专业类从通用类继承属性与函数。

派生类继承自基类的可访问数据字段和函数,同时也可添加新的数据字段和函数。

声明#

class A: public B {
...
}

其中B为基类,A为派生类。

protected关键字与访问权限#

私有(private)、受保护(protected)和公有(public)这三个关键字被称为可见性或访问权限关键字,因为它们指定了类及其成员的访问方式。它们的可见性依次递增.

与成员访问权限相对应的,类的继承也存在对应的权限。

泛型编程(Generic Programming)#

派生类的对象可以在任何需要基类参数对象的地方传递。因此函数可以广泛地用于处理各种对象参数,这种编程方式称为泛型编程。

比如,你可以将圆形或者矩形作为基类,传递给计算面积的类。

构造函数与析构函数#

正如同每个类都存在构造函数(即使你不明确指定),每个类也存在与之相对应的析构函数。

构造函数在类被实例化(也就是通俗意义上的创建)时调用,而析构函数在实例被删除(通常是程序结束或手动执行)时调用。

析构函数与构造函数命名相同,但必须在前面添加波浪号(~):

Circle.h
class Circle{
public:
    Circle();
    Circle(double r);
    ~Circle();
   
}
Circle.cpp
Circle:: ~Circle(){  …  }
Important

派生类的构造函数在执行自身代码之前,会先调用其基类的构造函数。 派生类的析构函数执行自身代码后,会自动调用其基类的析构函数。

可以在派生类的构造函数初始化列表中调用基类的构造函数。

DerivedClass(parameterList): BaseClass(){
    // Perform initialization
}
DerivedClass(parameterList): BaseClass(argumentList){
    // Perform initialization
}

在部分情况下(比如基类有无参的构造函数,或者有参但可以使用默认值),第一种情况下的BaseClass()可以省略不写,这时候c++编译器会自动调用默认的构造函数(无参/默认值)。

另一种构造方法:

Circle::Circle(double radius, const string& color, bool filled)
: GeometricObject(color, filled), radius(radius){
} // 调用了 GeometricObject的构造函数,同时对GeometricObject的radius初始化值

由类看向OOP - 多态(函数重定义与多态)#

函数重定义(静态绑定)#

与函数重载类似,在基类中被定义的函数也可以在派生类中被重定义。

string GeometricObject::toString() const{
    return "Geometric Object";
}
string Circle::toString() const{
    return "Circle";
}
string Rectangle::toString() const{
    return "Rectangle";
}

假设其中的CircleRectangle类继承于GeometricObject类,则对其调用toString方法默认会执行派生类中重定义后的函数。

GeometricObject g;
cout << g.toString() << endl; // "Geometric Object"
Circle circle(5, "red", true);
cout << circle.toString() << endl; // "Circle"
Rectangle rect;
cout << rect.toString() << endl; // "Rectangle"

如果要对实例化的派生类操作,使其执行基类的该函数,可以:

circle.GeometricObject::toString()

多态#

多态指同一个行为在不同对象上表现出不同的形式或结果。

void displayGeometricObject(const GeometricObject& shape) {
    cout << shape.getColor() << endl;
}
int main(){
    displayGeometricObject(GeometricObject("black", true));
    displayGeometricObject(Circle(5));
    displayGeometricObject(Rectangle(2, 3));
    return 0;
}

派生类的对象可以在其基类对象使用的任何地方使用。

虚函数和动态绑定#

一个函数可以在继承链中的多个类中实现。虚函数使系统能够根据对象的实际类型在运行时决定调用哪个函数。

也就是说:你只需要定义一个函数,剩下的交给实际情况自行判断。

void displayGeometricObject(const GeometricObject& shape) {
    cout << shape.toString() << endl;
}

注意:

  1. 该函数必须在基类中定义为虚函数。
  2. 在虚函数中,引用对象的变量必须通过引用传递或作为指针传递。
void displayGeometricObject(const GeometricObject& shape) {
    cout << shape.toString() << endl;
}
// or
void displayGeometricObject(const GeometricObject* shape) {
    cout << (*shape).toString() << endl;
}
特性函数重定义 (Redefinition)虚函数重写 (Virtual Override)
关键字不需要virtual需要virtual(基类)和override(可选,推荐)
绑定方式静态绑定(编译时)动态绑定(运行时)
多态性不支持多态支持多态
隐藏规则隐藏基类同名函数覆盖基类函数
调用决定由指针/引用类型决定由实际对象类型决定
性能更快稍慢(虚表查找)
设计用途修改基类行为,但不通过基类接口调用实现运行时多态,通过基类接口调用派生类函数
示例代码void func() { ... }virtual void func() { ... } void func() override { ... }
访问基类版本使用作用域解析符:Base::func()不能直接访问被覆盖的版本
虚函数表不涉及虚函数表通过虚函数表(vtable)实现
适用场景工具类、不希望多态时、性能优化框架设计、接口抽象、多态需求

抽象类与纯虚函数#

抽象类看着很像一般类,但是不能用于创建对象。抽象类包含抽象函数,这些函数在具体派生类中实现。

一个类如果包含至少一个纯虚函数,那么它就是抽象类。

纯虚函数是在基类中声明但没有定义的虚函数,它的声明方式是在函数声明的末尾加上 = 0

virtual double getArea() const = 0;

纯虚函数没有函数体。

抽象类和纯虚函数的作用#

  • 定义接口:抽象类可以定义一组接口,强制派生类实现这些接口。
  • 实现多态:通过基类指针或引用调用纯虚函数,实际执行的是派生类中重写的函数。
  • 代码复用:抽象类中可以包含非纯虚函数和成员变量,为派生类提供可复用的代码。

文件#

正如<iostream>用来处理标准输入/输出流一样,文件输入输出也有与之对应的头文件: <fstream>.

写入文件#

在写入文件之前,需要先实例化一个ofstream类。然后对于这个类,使用和cout类似的方式输出。

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream output;
// Create a file
output.open("scores.txt");
// Write two lines
output << "John" << " " << "T" << " " << "Smith"
<< " " << 90 << endl;
output << "Eric" << " " << "K" << " " << "Jones"
<< " " << 85;
output.close();
cout << "Done" << endl;
return 0;
}

读取文件#

在读取文件之前,需要先实例化一个ifstream类。然后对于这个类,使用和cin类似的方式输入

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ifstream input;
// Open a file
input.open("scores.txt");
// Read data
char firstName[80];
char mi;
char lastName[80];
int score;
input >> firstName >> mi >> lastName >> score;
cout << firstName << " " << mi << " " << lastName << " "
<< score << endl;
input >> firstName >> mi >> lastName >> score;
cout << firstName << " " << mi << " " << lastName << " "
<< score << endl;
input.close();
cout << "Done" << endl;
return 0;
}

一些文件操作需要注意的细节#

如无特殊说明,下面均使用file 代指实例化的ifstream/ofstream类。

文件的打开/关闭#

  • 打开:使用file.open(path).
  • 关闭:使用file.close().

测试文件存在/可用性(对于读取)#

在使用open函数后,立刻使用file.fail()函数。如果返回true,说明文件不存在。

绝对路径与相对路径(略)#

Tip

对于windows操作系统下的反斜杠路径,注意转义。

测试EOF (对于读取)#

使用file.eof().

其他#

Important

文件读取的类型要求与cin保持一致,必须实现知道读入的数据类型,才能正确读取。

学习cout时,使用<iomanip>进行输出格式化的方法在文件输出流中依然可用。

getline, get 和 put#

和cin一样,文件输入流也存在遇到空格就停止的问题。如果想要读取一行,最好的方法仍旧和之前的处理方式一致:使用getline()函数,不过参数有所不同:

char a[100];
file.getline(a,99,'#')

读取单个字符或字符数组,仍然可以使用get() 函数;

// 读取单个字符
char ch;
file.get(ch); // 读取一个字符到ch
// 或使用返回值
int ch = file.get(); // 返回字符的int值,EOF时返回-1
// 读取字符数组(类似getline但不丢弃分隔符)
char buffer[100];
file.get(buffer, 100, '\n'); // 读取遇到换行符停止,但换行符留在流中

写入单个字符,使用put()函数:

#include <fstream>
ofstream outfile("output.txt");
outfile.put('A'); // 写入字符'A'
outfile.put('\n'); // 写入换行符
outfile.put(65); // 写入ASCII码65对应的字符('A')

fstream:两个愿望,一次满足!#

如果你想用同一个文件进行输入/输出操作,又不想开ifstreamofstream两个类,不妨来试试这种全新的做法吧!

这种用法需要手动声明对文件的操作模式,多个模式可以使用字符| 链接。

#include <fstream>
fstream file;
file.open("test.txt", ios::out | ios::app )
ModeDescription
ios::in打开文件用于读取
ios::out打开文件用于写入
ios::app所有输出都追加到文件末尾
ios::ate打开文件进行输出。如果文件已存在,移动到文件末尾。数据可以写入文件的任何位置
ios::trunc如果文件已存在,丢弃文件内容(这是 ios::out 的默认操作)
ios::binary以二进制模式打开文件进行输入输出

Template 与 vector#

Template:多个(不同类型)函数,一次满足!#

对于函数体相同但是仅仅输入 / 输出变量不同的函数,可以通过模板功能避免代码重复。

比如,对于下面的函数:

int maxValue(int a, int b) {
    if (a > b)
        return a;
    else
        return b; // 更简洁的写法: return a > b ? a : b
double maxValue(double a, double b) {
    if (a > b)
        return a;
    else
        return b;
char maxValue(char a, char b) {
    if (a > b)
        return a;
    else
        return b;
}

我们可以:

template<typename T>
T maxValue(T a, T b) {
    if (a > b)
        return a;
    else
        return b;
}

避免代码重复。新技能GET!

类模板#

对于类,也有与之类似的做法。

C++ 类模板是泛型编程的核心,它允许程序员编写一次类定义,然后用于多种数据类型。

  1. 将类定义与类实现合并为单一文件。
  2. 定义部分:
  • 在类定义前添加模板前缀。
  • 类型参数可在类中像普通数据类型一样使用。
  1. 实现部分:
  • 在构造函数和函数头前添加模板前缀,并在函数中用类型参数(T)替换常规数据类型。
  • 作用域解析运算符::前的类名应为ClassName<T>

代码示例如下:

//Definition
template<typename T>
class Stack{
public:
    Stack();
    bool isEmpty() const;
    T peek() const;
    void push(T value);
    T pop();
    int getSize() const;
    void display() const;
private:
    T elements[100];
    int size;
};
//Implementation

C++标准模板库:STL (Standard Template Library)#

标准模板库(STL)定义了基于模板的强大可重用组件,这些组件实现了多种常用数据结构及其处理算法。

vector#

构造方式:(需要引入vector头文件)

#include <vector>
vector<int> intVector; // 最简单的构造
vector<T> v,    // empty vector
v1(100),       // 100 elements of type T
v2(100, val),  // 100 copies of val
v3(fptr,lptr); // contains copies of elements in memory locations fptr to lptr

vector的相关函数#

获取关于vector的内容信息#

intVector.size() // 获取大小(实际包含的元素个数)
intVector.empty() // 获取是否为空
intVector.capacity()// 获取容量(在不增大内存空间的情况下最多可以存放的元素个数)
intVector.reserve() // 预先分配容量,减少内存分配和拷贝操作来提升性能

添加、删除、访问元素#

intVector.push_back() // 在尾部添加元素
intVector.pop_back() // 在尾部删除元素
intVector.front() // 返回对首个元素的指代
intVector.back() // 返回对末尾元素的指代

vector与迭代 (考试不考,略,后补充)#


完结撒花!#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

程序设计原理复习笔记
https://blog.murongpig.site/posts/programming-fundamentals-reviewing/
作者
MuRongPIG
发布于
2026-01-13
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
MuRongPIG
自无梦的长夜亮起,绽放在终竟的明天。
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
5
分类
3
标签
7
总字数
23,469
运行时长
0
最后活动
0 天前

目录