指针

前言

计算机的内存空间中最小单位是bit(位),其值只能是0或1。字节(byte)是计算机中的基本单位,一个字节由8个bit组成。一般计量都是以字节为单位的,比如每个char类型数据占1个字节,int类型数据占4个字节,double类型数据占用8个字节。

在操作系统中,所有内存单元都被编上了序号,通过这些编号(即内存的地址),可以快速找到内存单元,更灵活、高效的管理数据。

概念

计算机中的数据都是存储在内存中的,每条数据都有自己的地址,指针就是用来记录地址的变量。 img

图中 c 是一个字符型变量,值为 'K',存储在地址为 0x011A 的内存单元中。

p 是一个字符型指针变量,其值为 0x011A,也就是变量 c 的内存地址。在这种情况下,可以说 p 是一个指向变量 c 的指针。

指针的值是一个内存地址,这个地址可以是变量的首地址,也可以是数组的首地址;指针的类型表示指针所指地址上存储的数据的类型。

定义

格式:类型 *指针名;

int *p1;
double *p2;
char *p3;

使用

一、确定指针的指向

指针的值必须是一个地址,为保证它的有效性,可以借助取地址符“&”获取某个变量的地址。

int a;
int *p1 = &a;//定义指针的同时为其初始化一个地址
int *p2;
p2 = &a;//先定义个指针,需要用的时候才指定一个地址

【注意事项】

  • 两种方法是等价的,不过推荐使用第一种方法,这样可以保证一开始指针就是有效的。未经初始化的指针将指向一个不确定的内存地址,操作这样的指针是非常危险的。
  • 注意,在上面指针的定义中,指针变量是 p2 而不是 *p2,因此要给指针赋值,等号左边是 p2 而不是 p2。另外,不允许把一个无效的地址(比如数字)赋给指针。

指针变量和普通变量是一样的,存放的值是可以改变的,可以改变指针的指向。

int a, b;
int *p = &a; // 指针 p 指向变量 a
p = &b;      // 修改 p 的指向,令它指向变量 b

二、获取指针指向的数据

需要指针所指向的数据时,可以使用解引用操作符“*”得到指针所指向的变量。

int x = 3;
int *p = &x;
cout << x << ' ' << *p << '\n';
//直接使用x和间接的访问x
x = 0;// 同 *p = 0;
//两个赋值语句是等效的

三、指针的算数运算

指针加减某个整数,是将该指针移动整数个变量的长度。

例如p + j是将指针p向后移动j个单位(每个单位大小为指针p类型的字节数),指向p原先指向元素后的第j个元素。

#include <bits/stdc++.h>
using namespace std;
int main(){
	int a[5] = {1, 2, 3, 4, 5};
    int *p = a;
    cout << p << ' ' << *p << '\n';//0x73fdf0 1
    cout << p + 2 << ' ' << *(p + 2);//0x73fdf8 3
	return 0;
}
*p++ 先获取*p的数据后再让p+1
(*p)++ 先获取*p的数据后再让*p的数据自增1
*++p 先将p自增1,再获取*p的数据
++*p 先将*p的数据自增1,再获取自增后的结果

多维数组

声明与初始化

创建:数据类型 数组名 [行长度] [列长度];

cint arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};//其余未初始化的元素为0
int arr[][4] = {{2,3},{4,5}};

二维数组如果有初始化,行可以省略,列不能省略!!

我们可以把二维数组看成由多个一维数组组成的,只有在确定每个一维数组存放多少个元素才能确定二维数组。假设省略列,那么操作系统无法得知每个一维数组分配多少个元素,从而造成二维数组的混乱。

计算存储空间

占用空间 = 元素个数 * 一个元素所需内存大小。

类型名 存储空间
char 1 个字节
bool
short 2 个字节
float 4 个字节
int
long long 8个字节
double 8 个字节

使用

二维数组看似多行多列,实际上是连续存储的, 本质上还是个多个一维数组,如果把二维数组的每一行看作一个一维数组,那么每一行的一维数组也有数组名,arr[0]就是第一行的数组名,arr[1]就是第二行的数组名,以此类推。所以二维数组也可以通过下标的方式来引用每个元素。 格式:数组名[行下标][列下标];

int arr[3][4] = {{1,2},{4,5}, {7, 8}};
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        cout << arr[i][j] << ' ';
    }
}

下标

由于数组存储空间是连续的,那么下标也可以指距起始位置的偏移量。 数组元素的存储地址 = 数组空间首地址 + 偏移量 * 一个元素内存空间。

img

以行为主序的二维数组 a[m][n]a[m][n](下标从 0 开始),求 a[i]ja[i]j 对于前i−1i−1行,共有ii行,每行nn个元素,一共有i∗nin个元素;对于第ii行, 之前有jj个元素; 故a[i][j]a[i][j]的存储位置相对于数组空间首地址偏移量为i∗n+jin+j

img

以行为主序的二维数组 a[m][n]a[m][n](下标从 1 开始),求 a[i]ja[i]j 对于前i−1i−1行,共有i−1i−1行,每行nn个元素,一共有(i−1)∗n(i−1)∗n个元素;对于第ii行,之前有j−1j−1个元素; 故a[i][j]a[i][j]的存储位置相对于数组空间首地址偏移量为(i−1)∗n+j−1(i−1)∗n+j−1。

函数

前言

“函数”将实现了某种功能,或者需要反复使用的代码包装起来形成一个功能模块,当程序中需要执行该项功能时,只需写一条语句调用实现该功能的 “函数”即可。

函数

定义格式

函数返回类型 函数名(形式参数1,形式参数2...){
    函数体;
}
  • 返回类型有:int、double、char、bool、string、void或结构体类型等,其中void代表无返回值,可以不返回信息,其他类型必须在函数的最后return对应类型的信息;
  • 参数的书写格式为:数据类型 变量名,符合先定义后使用的规范;
  • return后是返回具体的一条信息,不可以是多个。
//判断函数是否为奇数
bool isodd(int x) {
    return x % 2 ? true : false;
}

调用

函数名(实际参数1,实际参数2...);

  • 调用函数的结果由该函数的返回值决定
  • 实际参数多数情况下要保证与形式参数的个数一致
  • 实际参数无需添加数据类型
int x = 3;
if (isodd(x)) {
    cout << "YES";
} else {
    cout << "NO";
}

传递

传值

#include <iostream>
using namespace std;
void change(int a, int b) {
	int t = a;
	a = b;
	b = t;
	cout << a << " " << b << endl;
	return;
}
int main() {

原理:传值过程中实际上是复制x的值至形参a,复制y的值至形参b,在change函数内部只是让a与b的值互换,而并未使得主函数的x和y交换。

结论:传值过程中修改形参的值并不影响实参的情况!

传引用

void change(int &a, int &b) {
    ...;
}

在数据类型和变量名称间添加&后,使得该变量成为引用变量。引用并不是一个独立定义的变量或其他值,而是一个对已存在的变量或其他值取了一个别名。

int a = 3;
int &b = a;
cout << a << ' ' << b; //3 3

可以将别名理解为取外号,如“小狗”被取外号叫做“修勾”...

这一期间编译器不会开辟新的空间,它与其引用对象共同使用同一块空间,对别名进行操作会影响引用对象。

结论:传引用中修改形参的值会影响实参的情况!

传地址

调用函数时把主函数中实参的地址传递给函数的形参,直接对实参地址所指向的内存空间进行操作。

#include <bits/stdc++.h>
using namespace std;
void change(int *x, int *y) {
	int t;
	t = *x;
	*x = *y;
	*y = t;
	cout << *x << " " << *y << endl;//5 3
	return ;
}

img 【结论】传地址过程中修改形参发生变化会影响实参的结果!

变量作用域

全局变量

定义在函数外部的变量称为全局变量,从定义变量的位置开始到程序结束始终有效,可以为程序中所有函数共用(若在定义时若没有赋初值,其默认值为0)。

局部变量

定义在函数内的变量叫做局部变量。局部变量的存储空间是临时分配的,当函数执行完毕后,局部变量的空间就被释放。不同函数的局部变量相互独立,不能访问其他函数的局部变量。

使用原则

遵循 “就近原则”

#include <bits/stdc++.h>
using namespace std;
int x = 3;
void test() {
	int x = 8;
	cout << x << ' ';
	return ;
}
int main(){
	x = 5;
	cout << x << ' ';
	test();
	cout << x << ' ';
	return 0;
}
//输出结果为5 8 5

排序

冒泡排序

原理:从前往后(或从后往前)两两比较相邻元素的值,若为逆序(即a[i]>a[i + 1]),则交换它们,直到序列比较完。每一轮结束时会在待排序元素中,将最大(或最小)的元素排序至正确的位置。

img

//将数组a从中的n(1-n)个元素从小大升序排序 
void bubble_sort(int a[], int n) {
	for (int i = 1; i < n; i++) { //仅需 n - 1轮 
		bool flag = true;//可以提前结束 
		for (int j = 1; j <= n - i; j++) { //每轮仅需n - i次比较 
			if (a[j] > a[j + 1]) {
				swap(a[j], a[j + 1]);
				flag = false;
			}
		}
		if (flag)	return ;
	}
	return ; 
}

选择排序

原理:在待排序序列中头至尾找出最小的一个关键字,与待排序序列中第一个关键字进行交换,重复此操作,最终使序列有序。

img

//将数组a从中的n(1-n)个元素从小大升序排序 
void select_sort(int a[], int n) {
	for (int i = 1; i < n; i++) {
		int k = i;//最小元素的位置 
		for (int j = i + 1; j <= n; j++) {
			if (a[j] < a[k]) {
				k = j; //找出最小元素的位置 
			} 
		}
		swap(a[i], a[k]);
	}
	return ; 
}

插入排序

原理:每次将一个待排序的关键字按大小插入前面已排好的序列中,直到全部关键字插入结束。 img

//将数组a从中的n(1-n)个元素从小大升序排序 
void insert_sort(int a[], int n) {
	for (int i = 2; i <= n; i++) {
		int t = a[i], j = i - 1;
		while (a[j] > t && j >= 1) { //判断是否可以插入到第j个元素之后 
			a[j + 1] = a[j];
			j--;//继续向前比较 
		}
		a[j + 1] = t;
	}
	return ; 
}

【总结】

将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。为了查找方便,计算机中的数据表都希望是按照关键字有序的,一般按照递增或递减方式排列。 img

衡量标准

1.时间复杂度 2.空间复杂度 3.稳定性

img

小知识

C++的异常处理机制,通过try、catch和throw三个关键字,为开发者构建了一个结构清晰、易于理解的异常处理框架。

当程序执行到可能抛出异常的代码段时,可以使用try块将其包围起来;

随后,通过一个或多个catch块来捕获并处理可能发生的特定类型的异常;

而throw关键字则用于在程序中显式地抛出异常,通知上层调用者当前代码遇到了无法继续执行的情况

C++异常概念

传统的错误处理机制:

终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。 返回错误码,缺陷:需要程序员自己去查找对应的错误。

如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的;

catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕;

try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。