一.指针基础
1.指针定义的理解
就像我们住房子划分房间一样,在系统中,内存会被划分为多个内存单元,一个内存单元大小是一个字节,我们在搜索房间时有门牌号,与之类似,每个内存单元都会有一个编号 = 地址 = 指针,计算机中把内存的编号叫地址,C语言中给地址起了新名字指针
2.变量的创建和指针的指向
变量创建的本质是在内存中开辟一块空间
int main() {
int a = 10; //变量创建的本质是在内存中开辟一块空间
//分配了四个字节,取地址是第一个字节的地址(较小的)
printf("%p",&a); //&取地址操作符 %p专门打印地址的(十六进制的形式打印的)
//printf("%X", &a);
return 0;
}
在上面,我们定义了一个int类型的变量a,而当a创建时,就要为a分配空间,int型分配四个字节,我们使用&(取地址符号)获得的就是a的第一个字节(最小的字节)的地址
假设内存中这样存放a,我们取地址取出的就是0x0012ff40
3.指针的书写和含义
那么我们将a的地址取出来后,怎么把它放到一个变量里呢?
我们这样来书写:
int a = 10;
int* pa = &a;
int * pa中,*代表pa是指针变量,int代表该指针指向的类型是什么(a的类型)
那么,既然指针也是一个变量,那么指针变量的大小是多少?
4.指针变量的大小
指针变量需要多大的空间,取决于存放的是什么(地址),地址的存放需要多大的空间,指针变量的大小就是多大(与指针指向的内容的类型无关)
我们可以写出如下代码来观察指针的内存
int main() {
int a = 20;
char ch = 'w';
char* pc = &ch;
int* pa = &a;
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(int*));
return 0;
}
分开写结果如下:
5.指针类型的意义
我们已经知道了*代表这是一个指针变量,那么前面指向的类型又有什么作用呢?
事实上,指针类型决定了指针在进行解引用的时候访问多大的空间,例如,我们在进行解引用操作时,int*解引用访问4个字节,char*解引用访问1个字节
分析图如下:
当我们使用char *去指向int类型时,解引用只会访问一个字节的内容,而我们知道int类型是四个字节,所以出现了错误
6.指针相加减
指针类型决定了指针的步长,向前或者向后走一步大概的距离
int main() {
int a = 10;
int* pa = &a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
char* pc = &a;
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
对于int*类型的指针变量,+1会移动四个字节,char*类型的指针变量,+1会移动一个字节,因此,我们要根据实际的需要,选择适当的指针类型才能达到效果
7.void*指针
无具体类型的指针(范性指针)可以接受任意类型的地址,不能进行解引用的操作,和指针的+-操作
int main() {
int a = 10;
int* pa= &a;
void* pc = &a;
char b;
void* pd = &b;
return 0;
}
8.const和指针
我们知道,const表示常属性,不能被修改,例如:
int main() {
const int n = 10; //n是变量,但不可以改变,常变量
//如果不想让n变,就在前加const
printf("%d", n);
return 0;
}
此时,n是无法改变的
但是我们可以使用指针来更改数字,如下:
int main() {
const int n = 10;
int* p= &n;
*p = 20;
printf("%d", n);
return 0;
}
这样,我们就可以改变n的变量
const修饰指针有两种情况,分为const在*前和const在*后,
1.const放在*的左边,修饰的是*p const int* p
2.const放在*的右边,修饰的是p int* const p
两者有什么区别呢?
int main() {
const int n = 10;
int m = 100;
//const int(* p) = &n;
*p = 20; //不能改变对象的值了
// p = &m; //可以改变指向的对象
//int* const p = &n;
p = &m; //不可以改变指向的对象
//*p = 20; //可以改变对象的值
//p是指针变量,里面存放了其他变量的地址
//p是指针变量,*p(n)是指针指向的对象
//p是指针变量,p也有自己的地址&p
return 0;
}
const放在*前,会管整个(*p),使其不能改变值,但是p不受管理
const放在*后,只管p,不能改变指向的对象,但是可以改变*p
9.指针的运算
指针的基本运算分为三种:
指针+-整数
指针 - 指针
指针的关系运算
指针加减整数运算
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组在内存中连续存放,随着下标增长,地址由低到高变化的
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d\n", *p);
p++; //p = p + 1
}
return 0;
}
指针-指针 == 指针之间元素的个数(运算的前提条件是两个指针指向了同一块的空间)
int main() {
int arr[10] = { 0 };
printf("%d", &arr[9] - &arr[0]);
printf("%d", &arr[0] - &arr[9]);
//指针 - 指针的绝对值是指针和指针之间的元素个数
return 0;
}
指针的运算关系:两个指针比较大小
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
while (p < arr + sz) {
printf("%d\n", *p);
p++;
}
return 0;
}
10.野指针
野指针 指针指向的位置是不可知的(随机的,不正确的,没有限制的)很危险
野指针的成因:
//成因一:指针未初始化
int main()
{
int* p; //局部变量不初始化的时候,他的值是随机值
*p = 20;
printf("%d\n", *p);
return 0;
}
//成因二:指针的越界访问
int main() {
int arr[10] = { 0 };
int* p = arr[0];
int i = 0;
for (i = 0; i < 11; i++) { //指向了不属于的空间,且用指针访问了
*(p++) = i;
}
return 0;
}
//成因三:指针空间释放
int* test() {
int n = 100;
return &n;
}
int main() {
int* p = test(); //p存n的地址,但是空间回收了
printf("%d", *p);
return 0;
}
如何规避野指针
1.指针初始化 知道就直接赋地址,不知道就赋值NULL
2.小心指针越界
3.指针不在使用时,及时置为NULL,指针使用前检查有效性
4.避免返回局部变量的地址
int main()
{
//int a = 10;
//int* pa = &a;
//*pa = 20;
//printf("%d", a);
//int* p = NULL; //空指针,地址是0,不能赋值
//*p //err
int a = 10;
int* p = &a;
if (p != NULL) //检测指针有效性
{
*p = 20;
}
return 0;
}
那是否我们可以更简单的方法来检测指针的有效性呢?
11.assert断言
assert的作用:运行时确保程序符合指定条件,不合条件,报错并终止运行
形式如下 assert(表达式); 表达式如果不成立则会报错
当然,assert需要头文件
#include <assert.h>
上面的代码可以更简单的写成下面的形式
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL); //err
*p = 20;
printf("%d\n", *p);
return 0;
}
在这里,由于*p是NULL,直接报错并终止运行了
如果后面我们不再需要assert断言,我们不需要把所有写出的assert代码都手动删除,只需要在头文件位置写成:
#define NDEBUG
#include <assert.h>
即可使得assert失效
12.指针的使用和传址调用
(1)我们尝试用代码实现strlen()
size_t Mystrlen(const char* s)
{
size_t count = 0;
assert(s != NULL);
while (*s != '\0') {
s++;
count++;
}
return count;
}
int main()
{
//strlen()-求字符串长度
char arr[] = "abcdef";
size_t len = Mystrlen(arr); //字符串中\0前的字符个数
printf("%zd\n", len);
return 0;
}
(2)函数实现两个数的交换
void swap(int x, int y) {
int z = 0;
z = x;
x = y;
y = z;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(a, b);
printf("%d %d\n", a, b);
return 0;
}
我们写出代码发现a与b的值没有发生改变,实际上,实参变量传给形参时候,形参是一份临时拷贝,对形参的修改不会影响实参,因此,我们应该传地址过去,实现改变
void swap(int* x, int* y) {
int z = 0;
z = *x;
*x = *y;
*y = z;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(&a, &b); //传址调用
printf("%d %d\n", a, b);
return 0;
}
二.指针和数组
1.数组名的理解
在大部分情况下,数组名是数组首元素的地址,有两个例外:sizeof数组名,这里表示整个数组的大小,&数组名,这里的数组名也表示整个数组,取出整个数组地址
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", arr + 1); //跳过了一个元素
printf("%p\n", &arr[0] + 1); //跳过了一个元素
printf("%p\n", &arr + 1); //跳过了一个数组
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr[0]));
return 0;
}
2.指针表示数组
我们知道了,数组名实际上是数组第一个元素的地址,因此,指针表示数组可以写成
arr[i] == *(arr+i) == *(p + i) == p[i]
我们输出数组元素可以使用指针的表示:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//for (i=0; i < sz; i++) {
// scanf("%d", p + i);
//}
for (i=0; i<sz; i++) {
printf("%d ", *(p+i));
}
return 0;
}
3.一维数组传参的本质
一维数组传参传过去的是数组首元素的地址
void test(int arr[]) //本质上arr是指针
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz2);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz1);
test(arr);
return 0;
}
4.冒泡排序法
冒泡排序的思想
1.两两相邻的元素进行比较
2.如果不满足顺序就交换,满足顺序就找下一对
例如,现在给出我们一个数组,让我们将数组由小到大(升序)排列
过程大致如下
从头开始进行冒泡排序,一趟冒泡排序会排好最后一个的内容,我们如果需要排n个元素,最多需要进行n-1次,而且由于每次都可以排好一个,因此每次冒泡需要的次数越来越少
我们写出以下的代码实现了冒泡排序:
void bubble_sort(int arr[],int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;
int j = 0;
for(j=0; j<sz-1-i; j++)
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;
}
if (flag == 1)
{
break;
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
//实现排序,排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
print_arr(arr, sz);
return 0;
}
当然,我们所说的进行n-1次冒泡排序是我们考虑的最坏的结果,很多时候,在我们没有进行到n-1次时排序就已经完成,排序完成的标志是本次排序中没有进行过一次交换,因此,我们可以加入判断是否交换过来提前结束本次的排序
我们写出了如下函数:
void bubble_sort2(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
if (*(arr+j) > *(arr+j+1))
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
二级指针
一级指针指向的是其他变量,而二级指针是存放一级指针地址的
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
**pp = 20;
printf("%d\n", a);
return 0;
}