1. Cpp基础

注意

从 VS Code 复制代码到 Obsidian 时会出现空行增加和 Tab 缩进丢失问题, 可能是因为:

  • VS Code 复制时默认携带 HTML/RTF 格式(如语法高亮, 换行符样式), 而 Obsidian 在解析这些格式时会误判为额外内容, 导致空行增加;
  • VS Code 默认使用 CRLF(Windows 换行符), 而 Obsidian 更兼容 LF(Unix 换行符), 混合使用会导致换行异常.

所以建议, 在 Obsidian 中粘贴 VS Code 复制过来的代码时, 按下 Ctrl+Shift+V (Windows/Linux) 或 Cmd+Shift+V (macOS), 直接粘贴为纯文本.

1 pre.

1.1 procedures of compiling

​ editor:edit code

​ compiler:翻译为机器语言 object code(.o)

​ linking:link with lib && your code 可执行代码

cin.get(); // 读取下一次键击,防止.exe“一闪而过”

2 Start to Learn C++

C++对大小写敏感

2.1 function definition && function prototype(函数原型)

//an example of function prototype
double pow(double, double);
//an example of function definition
type function_name(arguments_type, arguments){
    statements;
    return xxx;
}

​ 函数定义不能嵌套;

2.2 iostream: standard input && output stream header(标准输入输出流库)

// if
using namepace std; //使用命名空间(少用, 以免冲突)
// then
cout << ... << endl; && cin >> variable; //(不推荐)
// else:
std::cout << ... << std::endl; && std::cin >> variable; //(推荐)

//attention:
cin >> s1 >> s2;
// ">>" can be interrupted by (space), 空格符被视为cin输入流的分割点
// means that if you input "hello world!", then s1="hello", and s2="world!"

2.3 关键字

2.4 pre-processor directives (预处理器命令)

2.4.1 header file && linking (头文件和链接)

//Declaration && Definition
//必须在变量和函数的定义之前声明,注意不同文件的定义可能冲突,两种方案:
extern int num;	//num在其他文件定义,且不是static(局部变量)
#include "header"	//num在头文件中定义
//同一文件中的同一作用域中,变量不能声明两次, 而const可以重复定义

2.4.2 Macro(宏)

#define PI 3.14	//声明常量自变量
//相比于const,这只是一种文本替换,在此用途下是不安全的

//支持参数的宏定义
#define max(a, b) (a > b ? a : b)

//当然,因为是文本替换,甚至可以用其“定义”函数而不“声明”:
#define print_pointer_of_num \
				int *ptr = &number; \
				cout << ptr <<endl;
int number = 10;
print_pointer_of_num;

//也可以做传递参数的版本:
#define print_pointer_of_num(num) \
				int *ptr = &num; \
				cout << ptr <<endl;
int number = 10;
print_pointer_of_num(number);

2.4.3 条件编译

//#if ... #endif 条件编译段可以框住代码段:
#if 0
#include "header.h"
#define PI  3.14
int a = 1;
#endif 	/// 中间三行代码相当于被注释了,但只需修改为 #if 1 就可以解除注释

//判断是否已经有相关的宏定义
#ifndef PI
#define PI 3.14
#endif
//等同于:
#if !defined(PI)
#define PI 3.14
#endif

//#elif #else也是可用的,记得结尾的 #endif

3 Simple Figure Type && Operation (简单数据类型和操作符)

3.1 basic variables

variables name // 变量名命名规则采取标识符规则, 即 :

3.1.1 basic variables

基本变量类型主要包括(s.d. 表示 significant digits(有效位数)) :

整型(bit) 符号前缀 字符&小整数 布尔类型 浮点数(bit&s.d.) 自动
short(16) signed char bool float(32&6) auto
int(32) unsigned wchar_t double(64)
long(>32) char16_t long double(80~128)
long long(>64) char_32_t

addition:
单独的 unsigned 指 unsigned int
cin 和 cout 将输入和输出视作 char stream,对于 wchar_t stream 应当用 wcin 和 wcout

3.1.2 sizeof && typedef

sizeof(类型名 or 变量名);	//内置函数sizeof()返回字节数(bytes)
typedef typename newname;	//typedef, 定义变量的别名

3.1.3 const(常量)限定符

//supposed to initialize when declaring(建议在声明的同时进行初始化),ex:
const int toes = value;

3.2 operators(操作符)

type操作符类型 符号
运算 +, -, *, /, %
关系 ==, !=, >, <, >=, <=
逻辑 &&, ||, !
位运算
自增自减 ++, --
运算赋值 =, +=, -=, /=, *=, %=
位操作赋值
条件操作 ?=
逗号(返回最右端操作数的值) <expression_1>, <expression_2>, ...
// ?= 条件操作符的运用
<expression> ? <when expression True> : <when expression False>;
//the result of <expression> is bool (表达式的值是布尔类型)
//可以嵌套

操作符优先级: 略(建议适当多使用括号)

// attention:int/int = int, 解决方式:
std::cout.setffixed, ios_base::floatfield; // fixed-point,阻止科学计数法表示

3.3 type conversion

3.3.1 implicit(隐式转换)

boolcharunsigned char short(wchar_t) unsignedshort(wchar_t)intunsigned intlong intunsigned long intfloatdoublelong double
//in general,转换方向是:低精度->高精度,短字节->长字节,signed->unsigned
//while, there's some special case:
//(1)右值转换成左值的类型, 比如float运算赋值给int变量
//(2)条件操作符 || 条件语句 将<expression>转换成bool:0->False

3.3.2 forced/explicit(强制/显式转换)

// ex.
typename(variable);	//C-style
(typename)variable;
//attention: 上述操作仅在表达式中暂时利用转换后的类型, 未改变原本对象的类型

int_object = static_cast<int>(float_object);	//C++-style, 将float对象转为int并赋值给新的int对象

其他强制转换类型的函数: const_cast, dynamic_cast, reinterpret_cast

3.4 注释与换行

//双斜线表示单行注释

/*
成对或多行注释使用 /* ... */
*/

//用 "\" 对代码进行换行:
xxxxxxxxxxxxxxxx \
    xxxxxxxxxxxxxxxxxxx;

4 Statements (语句)

4.1 simple statement

//空语句(NULL)
;

4.2 complex statement(复合语句)

4.2.1 条件语句

a. if

//if
if(<expression>){
    //when <expression>'s True
    xxxxxxx;
} else if(<semi-expression>){	//when <expression>'s False
    //when <semi-expression>'s True
    xxxxxxx;
}
//.........
else{
    //when all of the expreesions' False
    xxxxxx;
}

b. switch

//switch:适合枚举
switch(object){
    case 0:	//if object==0
        xxxxxxx;
        break;	//否则,将依次执行后面case下的语句xxxxxxx;(无视case条件),称为"fall-through"
    case 1:
        xxxxxxx;
        break;
    //......
    default:
        xxxxxxxx;
        break;
}

4.2.2 循环语句

a. while

//while
while(<expression>){
    xxxxxxxx;	//if <expression> is True
}

b. do...while

//do...while
do{
    xxxxxx;	//if <expression> is True
}while(<expression>);

c. for

//for
for(int i=0; i<10; i++){
    xxxx;
}

4.2.3 跳转语句

a. break, continue, goto

//break:jump out of one loop body

//continue:end this loop in advance,start to next loop straightly

//goto
label:
	xxxxxxxx;
if(<expression>){
    goto label;
}

5 Compound (复合数据类型)

5.1 array (数组)

5.1.1 静态数组

大小是固定的.

typename arrayName[arraySize];	//declaration
//attention: arraySize is necessary, which isn't a variable
char week[7];	//create array of 7 short, the last index is std::strlen(week) - 1
week[0] = 'm';	//assign value
int iarray[3] = {1, 2, 3};	//initializing
int iarray[3]{1, 2, 3};	//initializing, okay with c++11
int iarray[3] = {1, 2};	//initializing, non-assigned elements' going to be set to 0
int iarray[3] = {};	//initializing, all elements will be set to 0
char week[7] = "mtwsfss";	//initializing
int inarray[] = {2, 3, 4};	//initializing without arraySize, initialization is necessary
int num_elements = sizeof inarray / sizeof(int);	//the number of elements

// !not allowed operations
inarray = iarray;	//copy, but arrayName's not a left-value, so it's wrong
iarray = {3, 2, 1};	//assign all value instead of initializing

5.1.2 动态数组

内存分配

// malloc() 在内存空间堆(Heap)中分配指定字节数的内存
// 与作用域中在栈(Stack)分配内存的局部变量不同, 堆中的内存一旦分配, 就不会自动释放, 直到调用free()函数
int *arr = (int*)malloc(5 * sizeof(int));  //malloc分配5 * sizeof(int)大小的内存(分配单位是字节), 由于malloc()返回 void*, 所以要转为所需的 int* 
int *ptr = arr;
for(int i = 0; i < 5; i++){
	*ptr = i;
	cout << *ptr <<"";
	ptr++;  //可与上一行一同简写为: cout << *(ptr++) << "";
}
cout << endl;
free(arr);  // 最后用free()函数释放内存, 否则发生内存泄漏(Memory Leak)
arr = NULL; //释放后指针赋值为 NULL, 这是一个好习惯
int *numPtr = new int(3);  //和malloc类似, 但直接分配了int: 3的内存, 并且无需类型转换, 但new操作符仍旧创建的是一个指针; 也可以用 int *numPtr = new int, 表示只分配空间但未赋值
delte numPtr;  //释放

int *arr = new int[5];  // 类似malloc, 分配了一段能放下5个int大小的内存
int *ptr = arr;
for(int i = 0; i < 5; i++){
	*ptr = i;
	cout << *ptr <<"";
	ptr++;  //可与上一行一同简写为: cout << *(ptr++) << "";
}
cout << endl;
delte [] arr;  // 加上方括号[]表示释放整个指针类型大小的空间, 而不是只删除数组的第一个int元素
arr = NULL; //释放后指针赋值为 NULL, 这是一个好习惯
// 动态分配二维数组
#include <iostream>
using namespace std;
int main()  
{  
	const int dim = 3;  
	// 动态分配一维数组:
	int *ptr = new int[dim * dim];  
	// 然后将指针转成二维数组的指针, 也就是指向一维数组的指针(数组指针):
	int (*mat)[dim] = (int(*)[dim])ptr;  // int (*mat)[dim]声明一个dim大小的数组指针, ()表示*mat的优先级更高, 即声明一个名为mat的指针. (int(*)[dim])ptr 将 ptr转换为指向 int[dim] 的数组指针的类型, (*)表示明确为数组指针而不是指针数组.
	for ( int i = 0; i < dim; i++ ) {
		for ( int j = 0; j < dim; j++ ) {
			mat[i][j] = i * dim + j;
		}
	}  
	for ( int i = 0; i < dim; i++ ) {  
		for ( int j = 0; j < dim; j++ ) {
			cout << mat[i][j] << " ";
		}
		cout << endl;
	}  
	delete[] ptr;  	//释放内存时只需要操作一开始的一维数组指针就行, []的左右有没有空格都无所谓
	return 0;  
}
---------------------------------------
输出:
0 1 2 
3 4 5 
6 7 8

5.1.3 多维数组

// 初始化
int arr1[3][3] = { {0, 1, 2},
						 {3, 4, 5},
						 {6, 7, 8} };
int arr2[3][3] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
int arr3[2][2][2] = { { {0, 1},
								{2, 3} },
								{ {4, 5},
								   {6, 7} } };
								   
// 遍历: 使用多个计数器实现
int arr1[3][3] = { {0, 1, 2}, 
                  		  {3, 4, 5}, 
                   		  {6, 7, 8} };
for ( int i = 0; i < 3; i++ ) {
	for ( int j = 0; j < 3; j++ ) {
		cout << arr1[i][j] << " ";
	}
	cout << endl;
}

5.2 C-style string (C 语言风格字符串)

'' refer to char(ASCII), while "" refer to str.

Cstyle string need header: cstring

#include<cstring>
char str1[5] = {'0', '1', '2', '3', '\0'};	//a string must with '\0' at th end, otherwise called char array
char str2[5] = "0123";	//the same, and the '\0' is understood
char str3[] = "0123";	//let the compiler count itself
//attention: as counting the arraySize, '\0' inclued
"123""456";	//str concatenation

char str4[] = "0123";
std::strlen(str4)	//return 4, char num before \0 (it's not arraySize)
// 结尾一定要有个"\0"
const char *str1 = "Hello";
cout << (int)str1[5] << endl;  //输出int:0, 由'\0'转换得到(ASCII码为0)

C 风格的字符串是一个指针, 只有开头字符地址的信息, 所以需要空字符'\0'判断 string 是否结束.

5.3 vector (向量: 数组的替代, 动态数据结构)

vector need header: vector

//initializing ex.
#include<vector>
vector<int> vec0; //only declare, vec0 is null
vector<int> vec00(3); //declare a int vector with length of 3, it's elements are default initial value
vector<int> vec1(3, 100); //declare a int vector with length of 3, all of it's elements are 100

//some operations
vector<int> vec2(vec1);	//copy vec1 to vec2, 深拷贝deep copy
vec1.size();	//similar to string.length()
vec1.empty();	//return True when vec1's empty
vec1.push_back(99);	//similar to list.append(99) in python
vec1.pop_back();	//remove an element from the end
cout << vec[1] << endl;	//access elements by index

5.4 C++ style string

可以视为 vectorchar

C++ style string need header: string

//initializing ex.
#include<string>
string s1(3, 'a');
string s2("s2's value");
string s3(s2);	//copy

//we can apply string, same as vector<char>

s1 = s1 + s2;	//concatenate, 很方便

5.5 struct (结构体)

5.6 class (类)

C++面向对象编程.

6 Derived (派生数据类型)

6.1 pointer(指针)

//definition ex.
int *ptr1;	//unsafe declaration
int *ptr = NULL; //safe declaration, null pointer, its value is 0(or said 0x00000000). *跟着int被编译, 表明是一个int*对象.
int arr[5]={0};	int *ptr2 = arr; //safe declaration
int num = 0;  int *ptr = &num;
//! After definition, ptr means &num (address), while *ptr means num (value which is pionted to). At this moment, * is called "dereference operator"(解引用运算符).

typename *type1_ptr, *type2_ptr; //right
int* ptr1, ptr2; //prt1 is a pointer, while ptr2 is int

void *ptr; //a pointer, but we don't kown type that it points to. Usually uesd in certain function which handles systemic memory.

// basic operation 基本操作
int num = 0;
int *ptr = &num;
&num; //Get address取地址 ==ptr
*ptr; //dereference解引用 ==num

// arithmetical operation 算术操作
int array[5] = {0, 1, 2, 3, 4};
int *ptr = array;
*(ptr + 2); // == *(array + 2) == array[2] == 2
*(ptr - 2); //unsafe, program may crash
int *ptr1 = array + 1;
ptr1 - ptr // = 1, Pointer subtraction 指针相减
ptr - ptr1 // = -1

//指向const对象的指针
const int num = 3;
// int *ptr1 = &num; 错误, 普通指针不能指向const变量, 正确的定义方式:
const int *ptr2 = &num;  //初始化, 表示对象是const int*, 即指向const int的指针
// const 指针可以指向普通变量:
int num0 = 4;
ptr2 = &num0;
// *ptr2 = 4; 错误, const指针不能修改解引用后的值, 但可以修改指向的地址:
const int num1 = 5;
ptr2 = &num2;

//const 指针
int num1 = 3;
int num2 = 4;
int *const ptr1 = &num1;  //const指针初始化, 指向int的const指针
ptr1 = &num2; //错误, const指针不能修改指向的地址

//指向const的const指针
const int *const int ptr2 = num3;
ptr2 = num4;  //错误, 不能修改值; 也不能修改地址

//数组的指针和指针的数组
int arr[5] = {0, 1, 2, 3, 4};
int (*arrPtr)[5] = &arr; //数组指针, ()表示指针指向整个数组
int *ptrArr[5] = {&arr[0], &arr[1], &arr[2], &arr[3], &arr[4]}; //指针数组, 数组的每个元素都是int*指针
// (*arrPtr)[i] == arr[i] == *(ptrArr[i])
//arrPtr和*arrPtr代表的地址一致, 但用[i]下表引用后, 发现元素长度不一致

//指针的指针
int num = 3;
int *numPtr = &num;  //类型是int*
int **numPtrPtr = &numPtr;  //类型是int**, 即指针(的)指针 

//用const_cast修改const属性(增加或去除)
int intNum = 2;
const int &constIntNum = const_cast<int&>(intNum);  //const_cast后面必须跟引用或指针类型, 因为const修饰的是对象的储存而非具体值, 此处跟的是强制引用为int
int &IntNum = const_cast<int&>(constIntNum);  //也可以用于去除const属性
const int *ptr = &intNum;
int *nonConstPtr = const_cast<int*>(ptr);  //const_cast后可以跟指针, 此处去除了*ptr的const属性, 存为新指针nonConstPtr
int intNum1 = 10;
nonConstPtr = &intNum1;  //然后就可以修改值

//reinterpret_cast重新解读数据类型
int intNum = 0x00646362;
int *intPtr = &intNum;
char *str = reinterpret_cast<char*>(intPtr);  //将int*转换为了char*, 十六进制的64, 63, 62分别是b, c, d, 所以str是"bcd"

// 函数指针
// 见7.6

6.2 Alias(引用)

引用的本质是一个变量的别名(Alias), 因此一定要和某个变量绑定.

// 基本用法
int num = 3;
int &numRef = num;  // 引用的初始化. 此时 numRef = num = 3.
numRef = 4; // 此时numRef = num = 4, num也被修改 (numRef只是num的别名)
int num1 = 5;
numRef = num1;  //只是将num的值改为num1的值, 没有让numRef指代num1, 而始终是num的引用/别名

// const引用 (防止使用别名将const对象纂改)
const float pi = 3.1415926f;
const float &piConstRef = pi;  //const引用
float &piRef = pi;  //错误, 不能将非const引用绑定到const变量

6.3 堆(Heap) & 栈(Stack)

都是内存中一块有限区域, 可分配存放各种数据.

由程序员在程序中指定分配的。在使用了 malloc(或 new 之后,系统会从堆中查找大小合适的空位,并将地址返回,数据就储存在那里。
这有点像酒店,前台找到一个空房间后将房间的钥匙交给顾客,顾客就可以入住;而根据顾客的人数不同,前台也会寻找不同的房间分配给客人入住,如单人顾客将会住进大床房,而同一家庭的顾客会住进家庭房或套房。
堆的内存分配不像栈,它是随机分配的。可能会出现小块的空缺,导致塞不下大块数据,这也就是碎片化的问题。
由系统自动分配给局部变量或函数参数的,并且紧密地朝着一个方向分配。
我们可以把栈看成一个盒子,在里面放各种东西时,东西只能往上放。也就是说在局部变量定义的时候,系统把局部变量放到现有的其他数据的上面,到了作用域结束的时候把变量拿出来以销毁变量,接下来的局部变量又可以使用这块空间了。
除了栈和堆之外,C++程序还有别的内存分区,它们在程序运行前都是确定的。数据区用来存放全局变量、静态变量和字面常量,其中初始化和未初始化的变量分别放在两个区域,而只读的常量或字符串字面量等放在只读区域。代码区则是用来存放各个函数体的二进制代码,在程序执行的时候从中获取指令.

7 Functions (函数)

7.1 简介

7.1.1 定义

// 函数定义ex.
int max(int a, int b){
	return ((a > b) ? a : b);
}  // 不需要";"作为结尾

返回值类型 函数名(形参类型 形参1, 形参类型 形参2, ...){
	...  // 函数体
}

ReturnType FunctionName(Parameter1Type Parameter1, ...){
	...  // FunctionBody
}

// 函数签名(Function Signature)/接口(Interface): 函数体之外的函数名&形参&返回值这三者的整体, 用户可互动/看得见的部分(类似按钮/旋钮).

7.1.2 调用

// 函数调用ex.
# include <iostream>  // tips: include后面有没有空格都无所谓
using namespace std

int max(int a, int b){
	return ((a > b) ? a : b);
}

int main(){
	int a = 3;
	int b = 4;
	int c = max(a, b); //函数调用, 使用Function Call Operator "()" (函数调用操作符)
}

函数名(实参1, 实参2, ...)  // 实参(Argument)

7.1.3 作用域

#include <iostream>
#include<string>
using namespace std;
void printSomething() {
	string something = "Function(outside block)";
	{
		string something = "Function(inside block)";
		cout << "before printSomething, something is: " << something << endl;
	}
	cout << "after printSomething, something is: " << something << endl;
}
int main() {
	// printSomething中变量something的定义不可见
	string something = "main";
	printSomething();
	cout << "in main, something is: " << something << endl;
	return 0;
}
-----------------------------------------
输出:
before printSomething, something is: Function(inside block)
after printSomething, something is: Function(outside block)
in main, something is: main

7.1.4 参数

7.1.5 返回值

#include <iostream>
using namespace std;
void doNothingForOne(int num) {
	if ( num == 1 ) {
		return;  //提前返回
	}
	cout << "num is: " << num << endl;
}

int main() {
	doNothingForOne(0);
	doNothingForOne(1);
	doNothingForOne(2);
	return 0;
}
--------------------------------------
输出:
num is: 0
num is: 2

7.1.6 静态局部对象

和函数绑定的变量, 不会随着函数的返回而销毁, 同时作用域仍是局部的. (可用作计数器)

void printAndCnt(int num) {
	static int cnt = 0;  // 局部静态变量
	cnt++;
	cout << "打印数字:" << num << endl;
	cout << "函数被调用了" << cnt << "次" << endl;
}

7.2 Argument Passing(参数传递)

将实参(argument)传递, 赋值给形参(parameter).

7.2.1 按值传递

7.2.2 指针传递

形参为指针, 实参为指针.

// 指针传递实现swap
include <iostream>
using namespace std;
void swap(int *pa, int *pb) {
	int temp = *pa;
	*pa = *pb;
	*pb = temp;
	cout << "at the end of swap(), a is " << *pa << ", b is " << *pb << endl;
}

int main() {
	int a = 3;
	int b = 4;
	cout << "before swap, a is " << a << ", b is " << b << endl;
	swap(&a, &b);
	cout << "after swap, a is " << a << ", b is " << b << endl;
	return 0;
}
---------------------------------------
输出:
before swap, a is 3, b is 4
at the end of swap(), a is 4, b is 3
after swap, a is 4, b is 3

7.2.3 引用传递

形参为引用, 实参为值.

// 引用传递实现swap
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
	cout << "at the end of swap(), a is " << a << ", b is" << b << endl;
}
int main() {
	int a = 3;
	int b = 4;
	cout << "befor swap, a is " << a << ", b is " << b << endl;
	swap(a, b);
	cout << "after swap, a is " << a << ", b is " << b << endl;
	return 0;
}
----------------------------------------
输出:
befor swap, a is 3, b is 4
at the end of swap(), a is 4, b is3
after swap, a is 4, b is 3
#include <iostream>
using namespace std;
// 使用指针&引用 参数实现"额外的返回值"
// 通过指针参数修改min, 通过引用参数修改max
void getMinAndMax(int a, int b, int *min, int &max) {
	*min = a < b ? a : b;
	max = a > b ? a : b;
}
int main() {
	int a = 3;
	int b = 4;
	int min = 0;
	int max = 0;
	getMinAndMax(a, b, &min, max);
	cout << "max is " << max << endl;
	cout << "min is " << min << endl;
	return 0;
}
-------------------------------------
输出:
max is 4
min is 3

7.2.4 const 参数

放置写函数实现时修改了参数, 常用于指针和引用参数(语义清晰和代码规范).

#include <iostream>
#include <string>
using namespace std;
// const引用参数
char getLastChar(const string &str) {
	// str[str.length() - 1] = 's';  // 把这行注释掉程序就能正常运行
	return str[str.length() - 1];
}

int main() {
	string str = "hello";
	cout << "the last char of str is " << getLastChar(str) << endl;
	return 0;
}
--------------------------------
输出:
the last char of str is o

注意, 如果采取 return str[str.length()], 将返回空字符串 \0.

7.2.5 数组参数

数组名自动转换为指向第一个元素的指针.

#include <iostream>
using namespace std;
// 数组参数
void printArrAddr(int arrParam[]) {  //最好加一个 int size 参数(编译器忽略了数组的大小)
	cout << "the address of arrParam is " << arrParam << endl;
}
int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	cout << "the address of arr is " << arr << endl;
	printArrAddr(arr);
	return 0;
}
--------------------------------
输出:
the address of arr is 0x7ffcb576a130
the address of arrParam is 0x7ffcb576a130

7.2.6 main() 函数的参数

int main(int argc, char**argv) 中, argc 是命令行中输入参数的个数, argv 才是参数字符串的数组, argv[0]是项目名. 例如 ./program 10 20 --help 中,"./program""10""20""--help" 都是参数.

#include <iostream>
#include <cstring>  // For strcmp function
using namespace std;

int main(int argc, char**argv) { // 第二个参数等同于 char* argv[], 即字符串数组, 外层的 '*/'[]' 表示可以指向任意数量的 'char*' 元素(即参数数量可变); 内层的 'char*' 表示每个参数可以是任意长度的字符串(只要以 `\0` 结尾).
    // Check for --help parameter
    for (int i = 1; i < argc; i++) {  // Start from 1, skip program name (argv[0])
        if (strcmp(argv[i], "--help") == 0) {  // Compare C-style strings with strcmp
            cout << "Program function: Calculate the sum of all input numbers" << endl;
            cout << "Usage:" << endl;
            cout << "  " << argv[0] << " [number1] [number2] ..." << endl;
            cout << "  " << argv[0] << " --help  display this help information" << endl;
            return 0;  // Exit after showing help
        }
    }
    // Calculate sum if no --help parameter
    int sum = 0;
    for (int i = 1; i < argc; i++) {  // Start from 1, skip program name
        sum += atoi(argv[i]);  // atoi() convert C-style string to integer
    }
    cout << "The sum of all input numbers is: " << sum << endl;
    return 0;
}
---------------------------------------------------------------------------------------------------------------------
使用的运行命令是:
cd ./C++\ code/build
cmake ../..
make
echo -e "run case\n-----------------------\n-----------------------\n"
./case --help  // 使用--help参数
./case 1 2 3 4 5
echo -e "\n\n-----------------------\n-----------------------\n end case"
----------------------------------------------------------------------------------------------------------------------
输出:
(tf_env) siecho@siecho-book:~/Desktop/zj_obsidian/C++$ ./build.sh 
-- C++ Compiler: GNU
-- C++ Compiler Path: /usr/bin/c++
-- C++ Compiler Version: 13.3.0  //这几行输出来自CMakeLists的设置
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/siecho/Desktop/zj_obsidian/C++/C++ code/build
[100%] Built target case  // 这几行是make的过程
run case
-----------------------
-----------------------

Program function: Calculate the sum of all input numbers
Usage:
  ./case [number1] [number2] ...
  ./case --help  display this help information
The sum of all input numbers is: 15


-----------------------
-----------------------
 end case

7.3 函数返回值

7.3.1 返回值或对象

和参数传递是一样的赋值操作, 只不过是从内赋值到外.

一般对于函数返回值有两层的拷贝, 而传参只有一层. 如, 用 int a = func(b) 时, func() 函数的 return 语句先把要返回的变量拷贝到 func(b) 所代表的返回值中, 再把 func(b) 拷贝到a.

函数调用端 int func() 将返回值赋给变量 float variable 时, 会有隐式转换; 类型完全不匹配(无法隐式转换时)会报错.

7.3.2 返回引用

函数的返回值可以是引用类型.

// 返回局部对象的引用
#include <iostream>
#include <string>
using namespace std;
string &retLocal() {
	string ret = "Hello";
	string &retRef = ret;
	return retRef;
}

int main() {
	cout << "the return of retLocal() is " <<  retLocal() << endl;
	return 0;
}
-------------------------------------------------------
输出:
the return of retLocal() is Hello

7.3.3 返回指针

#include <iostream>
using namespace std;
int *allocIntArr(int size) {
	// 给一个int数组分配空间
	int *ret = (int*)malloc(size * sizeof(int));
	return ret;
}

void releaseIntArr(int *intArr) {
	// 清理数组占用的空间
	delete intArr;
}

int main() {
	int size = 5;
	int *intArr = allocIntArr(size);
	// 给数组赋值
	for ( int i = 0; i < size; i++ ) {
		intArr[i] = i;
	}
	// 打印查看数组元素
	for ( int i = 0; i < size; i++ ) {
		cout<< "print the " << i << "-th element of array: " << intArr[i] << endl;
	}
	releaseIntArr(intArr); // 与分配空间的allocIntArr(size)函数成对调用
	return 0;
}
------------------------------------------------------
输出:
print the 0-th element of array: 0
print the 1-th element of array: 1
print the 2-th element of array: 2
print the 3-th element of array: 3
print the 4-th element of array: 4

7.3.4 main() 函数的返回值

main() 函数的调用端是操作系统, 一般用 return 0; 作为 main() 的返回值表示程序运行状态正常.

7.4 函数声明

7.4.1 函数声明和函数定义

和变量一样, 函数的声明和定义可以分离. 一般将包含函数原型(或函数签名)的函数声明集中放在头文件 .h, 作为用户需要查看的接口集合. 一个头文件只要存在函数声明, 编译器就知道在这个文件 .h 中的这个函数是可用的, 然后在链接时会去其他文件 .cc / .cpp / ... 找有没有这样的函数.

注意: 在同一个作用域中相同的函数的定义只能出现一次, 否则报错"重定义(error: redefinition)". 但是重复声明是可行的, 函数声明也可以省略形参的名字, 只保留形参类型.

#include <iostream>
using namespace std;
// 函数重复声明
int max(int a, int b) {
	return a > b ? a : b;
}
int max(int a, int b);
int max(int, int);  // 重复声明
int main() {
	cout << "the maximum between a and b: " << max(3, 4) << endl;
	return 0;
}
----------------------------------------------------------------------
输出:
the maximum between a and b: 4

更常见的, 我们常常把函数声明 && 函数定义 && 主函数分别放在三个文件:

7.4.3.h:
#pragma once  //头文件也可以放一些常量
int max(int a, int b);
---------------------------------------------------------------------------------------------
7.4.3.cpp:
#include "7.4.3.h"  // 设置自动查找的路径后, 若路径内有该.h文件, 就可将该行注释掉
int max(int a, int b) {
	return a > b ? a : b;
}
---------------------------------------------------------------------------------------------
7.4.3_main.cpp:
#include <iostream>
#include "7.4.3.h"
using namespace std;
// 函数定义和声明不在一个文件中
int main() {
	cout << "the maximum between a and b: " << max(3, 4) << endl;
	return 0;
}
--------------------------------------------------------------------------------------------------------------------------
两个.cpp文件都要编译
--------------------------------------------------------------------------------------------------------------------------
输出:
the maximum between a and b: 4

7.4.2 默认参数

函数定义时可以预先给定一些 default 参数, default 形参在参数列表的末尾(变量形参之后), 实参与形参的顺序依旧是一一对应.

#include <iostream>
using namespace std;
void printDate(int day, int month = 12, int year = 2018) {  // default 形参放在后面
	cout << "today is " << year << "/" << month << "/" << day << endl;
}
int main() {
	printDate(30, 11);  // 实参与形参(含default)按顺序一一对应
	printDate(24);
	return 0;
--------------------------------------------------------------
输出:
today is 2018/11/30
today is 2018/12/24

7.4.3 inline 内联函数

在函数定义前加上关键字 inline, 这样的函数叫内联函数(inline function).

维度 普通函数 内联函数
声明方式 无需特殊关键字(如void func(); 需用inline关键字声明(如inline void func();
编译后形式 生成独立的函数代码块(有固定地址) 不生成独立代码块,函数体被 “复制” 到每个调用处
调用开销 有明显开销(上下文保存、跳转、恢复,约几个 CPU 时钟周期) 无调用开销(直接执行展开的代码)
编译器态度 严格按照 “独立函数” 处理,调用流程固定 inline是 “建议” 而非 “强制”—— 编译器可忽略(如函数体大、有递归时,会自动按普通函数处理)
头文件放置 声明放头文件(.h),定义放源文件(.cpp)(避免多重定义) 定义需放头文件(.h)—— 因为编译器需在调用处看到函数体才能展开
适用场景 函数体大(如超过 10 行代码)、调用不频繁、有递归 / 循环、需动态绑定(虚函数) 函数体小(如 1-5 行代码)、调用频繁(如循环内调用)、简单逻辑(如 getter/setter)
与虚函数的关系 虚函数可以是普通函数(支持动态绑定) 虚函数通常不能内联(因为虚函数调用需运行时确定地址,编译器无法提前展开)

7.5 Overloading (函数重载)

两个函数的功能相近, 只是形参的类型不一致, 则可以合法声明两个名称相同的函数. 函数重载即同一作用域中, 形参表不同的函数.

#include <iostream>
using namespace std;

bool isEqual(int a, int b) {
	return a == b;
}

bool isEqual(float a, float b) {  // 重载
	return abs(a - b) < 0.00001; // 随意指定一个判断阈值
}

int main() {
	int a = 3;
	int b = 3;
	float fa = 3.0f;
	float fb = 3.001f;
	cout << "two intergers ";
	if ( !isEqual(a, b) ) {
		cout << "aren't ";
	}
	cout << "equal." << endl;

	cout << "two float ";
	if ( !isEqual(fa, fb) ) {
		cout << "aren't ";
	}
	cout << "equal." << endl;
	return 0;
}
--------------------------------------------------------------------------
输出:
two intergers equal.
two float aren't equal.

7.5.1 函数重载的定义

内层作用域的重名函数的定义/声明会屏蔽外层函数的定义, 编译器不会搜寻外层的同名函数定义. 例如:

// 函数屏蔽
#include <iostream>
using namespace std;
int add(int a, int b) {
	return a + b;
}

int main() {
	{ 		// 内层作用域的函数声明屏蔽了外层可用的函数定义
		int add(int a, int b, int c);
		int a = 4;
		int b = 5;
		cout << "a + b = " << add(a, b) << endl;
	}
	return 0;
}

int add(int a, int b, int c) {
	return a + b + c;
}

会报错:
zz_figure/Pasted image 20250824205109.png
所以只有相同作用域中的函数才存在函数重载. 为区分函数重载与重定义(报错), 给出一些例子:

// 似是而非的函数重复定义
#include <iostream>
using namespace std;

// 默认实参不改变形参的性质(重定义)
int add(int a, int b) { return 0; };
int add(int a, int b = 0) { return 0; };
// typedef不改变类型的实质(重定义)
typedef int num;
int rcp(num a) { return 0; };
int rcp(int a) { return 0; };
// 非引用或指针类型的形参加const(重定义)
int neg(const int a) { return 0; };
int neg(int a) { return 0; };
// 引用和指针类型的加上const(重载)
int neg(const int* a) { return 0; };
int neg(int *a) { return 0; };

int main() {
	return 0;
}

此外, main()函数不可重载.

7.5.2 重载解析简介

重载解析指的是, 在函数调用时, 编译器会寻找一个和实参列表匹配的重载版本. 重载解析的步骤:

7.6 函数指针

7.6.1 创建和初始化

程序的指令也是一种储存在内存中的数据, 用户函数会作为独立的模块, 放在 main() 函数指令的不同位置, 所以调用函数时需要知道存放函数的起始地址, 并让程序跳转到该位置. 这个函数开始的位置就是函数的地址/函数入口, 函数指针就指向函数入口.

// 函数指针的创建
#include <iostream>
using namespace std;
int min(int a, int b) {
	return a < b ? a : b;
}

int main() {
	int (*fpIntInt)(int, int);  // 函数指针声明, 第一个括号是防止被编译器解读为"int*", 相当于只是声明了一个返回值为"int*"的函数fpIntInt(). 第二个括号内的形参个数和类型要匹配正确.
	fpIntInt = min;  // 函数指针赋值
	return 0;
}

需要定义多个相似的函数指针时, 可以使用 typedef 来简化函数指针的定义:

// 用typedef简化函数指针的定义
#include <iostream>
using namespace std;
int min(int a, int b) {
	return a < b ? a : b;
}
int max(int a, int b) {
	return a > b ? a : b;
}

int main() {
	typedef int (*fpIntInt)(int, int);  // 定义了一个叫fpIntInt的类型名, 他是一个有两个int参数, 返回值为int的函数指针类型.
	fpIntInt fpMin = min;
	fpIntInt fpMax = max;  // 简化了重复定义
	return 0;
}

7.6.2 应用

虽然函数指针是指针, 但在使用时不需要解引用操作符.

#include <iostream>
#include <string>
using namespace std;
int min(int a, int b) {
	return a < b ? a : b;
}
int max(int a, int b) {
	return a > b ? a : b;
}

int main() {
	int a = 0;
	int b = 0;
	cout << "please input two number separated by [space]: " << endl;
	cin >> a >> b;

	string cmd = "";
	cout << "please input your command (max or min):" << endl;
	cin >> cmd;

	typedef int (*fpIntInt)(int, int);
	fpIntInt fp = min;
	if ( "min" == cmd ) {
		fp = min;  // 使用函数指针, 切换到用户所选择的命令对应的函数
	} else if ( "max" == cmd ) {
		fp = max;
	} else {
		cout << "wrong command!" << endl;
	}
	cout << "the result is: " << fp(a, b) << endl;
	return 0;
}
--------------------------------------------------------------------
输出:
please input two number separated by [space]: 
1 2
please input your command (max or min):
max
the result is: 2

7.6.3 作为参数

函数指针作为一种数据类型, 所以也可以作为其他函数的参数. 例子:

#include <iostream>
#include <string>
using namespace std;
typedef int(*binaryFp)(int, int);  // 预先定义一个通用的函数指针类型名
int binaryOp(int a, int b, binaryFp binFp) {  // 接受函数指针作为第三个形参
	return binFp(a, b);  // 决定调用的二元运算函数是哪一个(add, sub or mul)
}
int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int main() {
	int a = 0;
	int b = 0;
	cout << "please input two number separated by [space]: " << endl;
	cin >> a >> b;
 
	string cmd = "";
	cout << "please input your command (add, sub or mul): " << endl;
	cin >> cmd;
 
	typedef int (*fpIntInt)(int, int);  // 定义一个通用的函数指针类型名, main()内使用
	fpIntInt fp = NULL;
	if ( cmd == "add" ) {
		fp = add;
	} else if ( cmd == "sub" ) {
		fp = sub;
	} else if ( cmd == "mul" ) {
		fp = mul;  // 使用函数指针, 切换到用户所选择的命令对应的函数
	} else {
		cout << "wrong command!" << endl;
	}
	cout << "the result is: " << binaryOp(a, b, fp) << endl;  // 第三个实参为函数指针
	return 0;
}
--------------------------------------------------------------------
输出:
please input two number separated by [space]: 
2 3
please input your command (add, sub or mul): 
sub
the result is: -1

7.6.4 作为返回值

#include <iostream>
using namespace std;
int (*funcRetFp(int a, int b))(int, int) {  // funcRetFp(int a, int b)是函数名和参数, int(* )(int, int)是返回值的类型
	int(*fp)( int, int ) = NULL;
	return fp;
}
typedef int(*binaryFp)(int, int);
binaryFp funcRetFpTypedef(int a, int b) {  // 和前面一个函数等价, 但通过使用typedef, 程序变得更可读
	binaryFp fp = NULL;
	return fp;
}
int main() {
	cout << funcRetFp(1, 2) << " " << funcRetFpTypedef(1, 2) << endl;
	return 0;
}
----------------------------------------------------
输出:
0 0

7.7 递归函数

一个函数可以循环调用自身, 称为递归(Recursion). 核心思路是"分而治之", 即把一个问题分解成许多小问题, 然后解决.

在每一次进入更深一层的递归函数时, 当前函数都会"放下手中的工作", 将本地变量 && 计算结果 && 函数状态存进栈里, 当所有内层函数都执行完毕后再返回. 由于栈是有限的, 所以递归函数中一定要有终止条件, 否则造成栈溢出(Stack Overflow).

#include <iostream>
using namespace std;

// 递归函数例子: 爬楼梯
int countWays(int n) {
	if ( n == 1 ) return 1;
	if ( n == 0 ) return 1;
	return countWays(n - 1) + countWays(n - 2);  // 走一个台阶, 就有n-1个台阶要走; 走两个台阶, 就还有n-2个要走, 于是 countWays(n) = countWays(n - 1) + countWays(n - 2).
}
int main() {
	int stairNum = 0;
	cout << "小强走台阶可以走一级,也可以走两级," << endl;
	cout << "请输入台阶数量:" << endl;
	cin >> stairNum;
	cout << "小强走台阶有"<< countWays(stairNum) << "种方法。" << endl;
	return 0;
}
--------------------------------------------------
输出:
小强走台阶可以走一级,也可以走两级,
请输入台阶数量:
6
小强走台阶有13种方法。

7.8 可变参数

包含 header <stdrag.h> (没错, 继承自 C 语言), 就可以定义一种函数, 其参数个数可变.

#include <iostream>
#include <stdarg.h>  // header needed
using namespace std;

// 形参表中, size为参数个数, 三点代表可变参数
float avg(int size, ...){
    float sum = 0.0;
	va_list valist;  // 创建可变参数列表
    va_start(valist, size);  // 初始化可变参数列表
    for ( int i = 0; i < size; i++ ) {
       sum += va_arg(valist, int);  // 用va_arg宏依次取得参数, 可变参数可以是不同类型的参数, 此处都是int (必须自己知道每一个参数的类型)
    }
    va_end(valist);  // 清理内存
    return (sum / size);
}

int main(){
    cout << "求1 2 3 4 5的平均数:" << avg(5, 1, 2, 3, 4, 5) << endl;
    return 0;
}
-----------------------------------------------------------------------
输出:
求1 2 3 4 5的平均数:3