1.assert断言
首先,assert.h头文件定义了宏assert(),用于在运行是确保程序符合指定条件,如果不符合就报错终止运行。这个宏常常被称为“断言”。
assert(p != NULL);
上面的代码在程序运行到这一句话的时候,验证p变量是否不等于NULL,如果确实不等于那么程序就继续运行,如果等于则终止程序,并且给出报错信息提示
assert()宏接收一个表达式作为参数。如果该表达式为真,assert()不会产生任何作用,程序运行。如果表达式为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()宏的好处:
1.能自动标识文件和出问题的行号
2.无需更改代码就能开启和关闭assert()(如果不需要断言了,只需有在#include<assert.h>语句定义NDEBUG)
#define NDEBUG
#include <assert.h>
2.指针的使用和调用
2.1 strlen的模拟实现
库函数strlen的功能是求字符串长度,统计的是字符串中\0之前的字符个数。
函数原型如下
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。
参考代码如下:
#include<assert.h>
int my_strlen(const char* str)//首字符的地址,const限制字符串被修改
{
int count = 0;
assert(str!=NULL);
while (*str!='\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdefg";
int len = my_strlen(arr);
printf("%d ", len);
return 0;
}
2.2 传值调用和传址调用
学习指针的目的是为了用指针解决问题,下面这个问题大家一起看看把,为什么这个只能用指针解决。
写一个函数,交换两个整数的变量
首先我们用非指针的方法去写一下这个代码。(其实就是传值调用)
void Change(int a, int b)
{
int c = 0;
c = a;
a = b;
b = c;
}
int main()
{
int x = 10;
int y = 20;
printf("交换前:x=%d y=%d", x, y);
Change(x, y);
printf("交换后:x=%d y=%d", x, y);
return 0;
}
我们运行后可以看出来,X和Y的值并没有发生交换,这是什么问题造成的呢?
我们在程序中调试下可以看到,只是在交换函数中,形参的a,b值发生了变化,但是x和y并没有变化。所以我们也可以知道,形参只是实参的一份临时拷贝,对形参的修改不会影响实参
接下来我们就试试用指针的方式调用,看能不能去交换这两个值也就是传址调用
void Change(int* pa, int* pb)
{
int c = 0;
c = *pa;//c=x
*pa = *pb;//x=y
*pb = c;//y=c
}
int main()
{
int x = 10;
int y = 20;
printf("交换前:x=%d y=%d\n", x, y);
Change(&x, &y);
printf("交换后:x=%d y=%d", x, y);
return 0;
}
我们可以看到这次通过传址的方式,实现了两个变量的交换。所以对传址调用做个总结
传址调用,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。
3. 数组名的理解
前面当我们想用指针访问的数组的时候我们可以这样写代码
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *p = &arr[0];
在这里我们使用&arr[0]的方式去访问数组中第一个元素的地址,但是其实数组名本身就是地址,而且是数组首元素的地址。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
输出结果:
很容易看出来,数组名就是数组首元素的地址
这里有两个特列需要牢记
1.sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2.&数组名,这里的数组名表示的是整个数组,取出的是整个数组的地址(和首元素地址一样)
除了这两个以外,其他任何情况使用数组名都是首元素地址的意思。
3.1&数组名和数组名的区别
&arr[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848
这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是 首元素的地址,+1就是跳过⼀个元素。 但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。
4.使用指针访问地址
例:使用指针的方式打印数组中的元素
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
5. 一维数组的传参本质
在讲解一维数组之前,我们先思考一个问题,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传递给一个函数后,函数内部求数组元素吗?接下来咱们写代码试试
void test(int arr[])
{
int sz2= sizeof(arr) / sizeof(arr[0]);
printf("sz2=%d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1=%d\n", sz1);
test(arr);
return 0;
}
从结果上看,在函数内部计算元素个数明显是错误的。问题出在哪里呢?
我们前面讲过,arr其实就是数组首元素的地址,那么在数组传参的时候其实传的就是数组名arr,也就是说数组传参本质上是传送的是首元素的地址
所以函数的形参部分理论上应该写成指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr)计算的是一个地址的大而不是数组的大小。正是因为函数的参数部分本质是指针,所以数组内部无法求得元素的个数
void test(int * arr)//参数写成指针形式
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2=%d\n", sizeof(arr));
}
总结:一维数组传参的时候,形参既可以写成数组的形式也可以写成指针的形式。
6.冒泡排序
冒泡排序核心思想就是:两两相邻的元素进行比较。
void bublle(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//趟数
{
for (j = 0; j < sz - i - 1; j++)
{
if (*(arr+j) > *(arr + j + 1))//几对比较,对数
{
int temp = 0;
temp = *(arr+j);
*(arr+j) = *(arr + j + 1);
*(arr + j + 1) = temp;
}
}
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
//排序
int sz = sizeof(arr) / sizeof(arr[0]);
//升序
bublle(arr, sz);
//打印
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
其实上面代码还是可以优化的,因为如果已经是有序的序列的话,其实就不用再进行比较了。根据这个思路我们可以这样优化。
void bublle(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//趟数
{
int flag = 1;//假设这一趟是有序的
for (j = 0; j < sz - i - 1; j++)
{
if (*(arr+j) > *(arr + j + 1))//几对比较,对数
{
flag = 0;//发生交换说明是无序的
int temp = 0;
temp = *(arr+j);
*(arr+j) = *(arr + j + 1);
*(arr + j + 1) = temp;
}
}
if (flag == 1)
{
break;
}
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
//排序
int sz = sizeof(arr) / sizeof(arr[0]);
//升序
bublle(arr, sz);
//打印
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
7. 二级指针
指针变量也是变量,是变量就有地址,那么指针变量的地址存在哪里呢?
答案是,存在二级指针
7.1 对于二级指针的运算
1. *ppa通过对ppa中地址的解引用,这样就找到了pa,*ppa其实访问的就是pa。
int b= 10;
*ppa = &a; //等价于 pa = &b;
2. **ppa先通过*ppa找到pa的地址,然后对pa再进行解引用,*pa,那就找到了a
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
8. 指针数组
在讨论指针数组之前,需要明确一点,指针数组究竟是指针还是数组呢?
我们做个类比,整形数组,我们理解是存放整形的数组叫整形数组
字符数组,存放字符的数组是字符数组
所以,指针数组其实就是存放指针的数组
指针数组的每个元素都是用来存放地址的(指针)
指针数组里面的每一个元素又可以指向一块区域
9. 指针数组模拟二维数组
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素地址,类型是int*,就可以存放再parr数组中
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]是访问parr数组元素,parr[i]找到数组元素指向了整形一维数组,parr[i][j]就是整形一维数组中的元素。这里的parr[i][j]等价于*(*(parr+i)+j)