目录
获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
编程实现:两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同
字符串
初级知识
字符串的结束标志是\0,arr1有隐藏\0,arr2的\0是随机的,他会不断寻找。
转义字符知识
他遇到\0就结束了,\0是字符串的结束标志。
转义字符表
三字母词
//??)-]
??(-[
太老了,已经不用了。
转义字符加\就行了。
我举一个打印转义字符的程序给你们看看
几个常见的打印形式
、
\ddd表示1-3个八进制的数字,如\130是X,是一个字符
\xdd,dd表示十六进制数字,如\x30是0,这个是一个字符
ASCII表
|注意\ddd和\xdd转义字符
操作符
我只说逗号表达式
逗号表达式是从左到右依次计算,整个表达式的结果是最后 一个表达式的结果
总结:除了单目操作符和赋值操作赋是从右到左,其他全部是左到右
typeder 和static的用法
这里的typeder是关键词,相对于重定义效果,uint=uisgned int num效果
typeder struct Node 变量名相对于等于Node变量名
static的用法
这的test的个局部函数,进入作用域生命周期开始,出作用域生命周期结束,这个a就是局部变量,出了这个生命周期就结束了,所以打印了十个2。
加了这个static修饰局部变量时,出了作用域不销毁,相对于static int a下次没有用。
静态区变量只会创建一次。
本质上static修饰局部变量时,改变了变量的存储位置
static不能在多个文件修饰全局变量。切记切记,一个文件可以修饰全局变量
总结:static修饰局部变量:
本质上影响变量的存储类型,一个局部变量是存再栈区的,但是被static修饰后就存在静态区了,因为存储类型的发生变化,生命周期跟着发生变化,变长了!。
总结·:static修饰全部变量时:
1.首先要知道全局变量具有外部链接属性的
2.全局变量被static·修饰后,外部链接属性就变成了内部链接属性,就是这个全局变量只能在自己的。c文件夹使用,其他文件看不到了,相当于作用域变小了。
static修饰函数
总结:
static修饰函数时,只能在自己的。c文件里使用,其他文件即使声明的也不能使用。
register是寄存器
就是访问速度快些,但不是全部放在这里。
指针的详解
我们可以把每个内存单元看成是一个编号,每个编号都有它相应的地址,这个地址就叫做指针
任何变量创建都会在内存开辟空间。
&a取a的首地址,比如图里的5地址处。还有下图的第一个地址
内存单元
编号-地址-地址也是指针
存放指针(地址)的变量叫做指针变量
p指向了a。也就是指针变量p存放了a的地址,类型为int。
指针就是来存放地址的
*p就是解引用操作符,通过p存放的地址找到p所指向的对象,*p就是p所指向的对象,就是找到了对象。
这里*p找到了a就可以修改了。
指针就是地址,
指针变量就是存放地址的。
区别:口头语的指针就是指针变量。
指针变量的大小
32位上int*,char* float*,long* double*等等,不管是什么类型的指针,都是在创建在指针变量,指针变量是用来存放地址,取决于一个地址存放的时候的空间多大。
32位的机器是32个bit位-4byte
64-32bit-8byte
所以32位机器的指针变量大小是4个字节,64位是8个字节。
指针的初级
把内存单元的编号称为地址,地址也叫指针。
指针就是内存单元的编号。
取地址的时候取的是最开头的地址。
因为知道起始地址后面的3个地址也容易找到。
总结:平时口头语的指针就是指针变量,实际的指针就是地址。
指针变量里面是存放的是地址,通过这个地址可以找到一个内存单元。
NULL是空指针,值为0
int类型
char类型
指针类型是有意义的:指针类型决定了指针在解引用的时候访问几个字节
如果是int*的指针解引用访问4个字节,char*的解引用访问1个字节。
指针的类型决定了+-1操作的时候跳过的几个字节
可以通俗的理解指针迭代步长
char*指针
int*类型指针
int*和float*的指针不能混用。
这个地址编号存起来需要4个或者8个字节,但是这个地址向后访问1个或者2个或者4个字节或者8个字节
总结指针大小是4个或者8个字节,但是指针的类型决定了向后访问的权限的大小
pa是指针变量他的大小取决于他存的地址的大小,与a的大小没有关系
这里的p就是野指针。
野指针的原因有二个
1.没有初始化
2.越界访问。
一个是地址+1 下面是值+1
指针-指针的绝对值=元素之前的个数。
只要拿到a的地址和\0的地址,就可以求字符串的长度。
//指针-指针求字符串长度方法
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
指针-指针的绝对值得到的是元素之间的个数。
允许你p1和p2比,不允许p1和p3比。
这个里的p存储的是arr首元素的地址,首元素的地址+i解引用相当于访问的是这个数组的i个元素
arr是数组的首元素地址 (arr+i)在解应用的时候可以访问下标为i的元素。
指针也是一样 p指向数组的首元素地址 (p+i)解引用就可以访问下标为i元素了。
a在内存中有自己的内存地址,pa虽然指向的a,存储了a的地址,但是pa也有自己的内存空间,同理ppa也是。
这个*说明pa是指针,int说明pa指向的对象是int类型
第二个*说明ppa是指针,第一个int*说明 ppa指向的对象pa的类型是int*
二级指针是用来存放一级指针变量的地址的
指针数组
存放指针的数组就是指针数组本质是个数组
在这里int arr[10] 这里的int 说明 arr存放的是整形类型 int * parr 这里的int* 说明parr存放的是整形指针类型。
parr[i]就是访问下标为i的元素, 就相当于拿a的地址或者b的地址或者c的地址 然后在解引用就可以访问a或者b或者c
通过下标找到a的地址,解引用找到a的元素等等。
parr[i]就是访问下标为i的元素, 就相当于拿arr1的地址或者arr2的地址或者arr3的地址,也就是拿到了他们的数组名,+j然后再解引用就可以访问它第i行第j个元素了。
arr1相对于第一行的数组名,也就是第一行的起始地址,加上j就可以访问第一行的某个元素的地址了。
const修饰
指针进阶
字符指针![ccb411a0b7df41dd8ccb6128557ed5ab.png](https://img-blog.csdnimg.cn/ccb411a0b7df41dd8ccb6128557ed5ab.png)
这个就是把字符串a的地址存在p里面了。
只要告诉我们这个字符串的起始地址,我们就可以向后打印,直到打印\0为止。
p1与p2都是指针,指向abcdef的起始地址,并且都是常量字符串,都是只读区域不能修改。所以一样
arr1与arr2虽然内容一样,但是开辟的内存首地址不同。因为arr1与arr2是不同的数组名。
指针数组
顾名思义就是存放指针的数组,本质就是数组。
指针数组 说明数组里的每个元素是int*类型的
arr1是parr【下标为1】的数组名,存放的是首元素(1)的地址。
arr1是数组名,arr1相当于于首元素1的地址
parr有三个元素,分别是arr1,arr2,arr3,每个元素的类型是int*的指针
只要parr找到对应的下标,就相当于对应的数组名,就可以访问对应的元素起始地址,
parr【i】就相当于arr1或者arr2或者arr3这个元素,然而arr1又是数组名,数组名又是首元素地址,就可以1访问1的地址,在加上一个j,在解引用就可以访问arr1里的某个元素了。
数组指针
用来存放数组的地址的
存放数组的指针,本质是指针
它的类型就是去掉名字,剩下的就是他的类型。数组指针类型。
*没有类型才叫解引用。
int*p1[10]这个p1是跟[]结合,说明p1是数组,int*说明里面存放的是整形指针。
int(*p2)[]这个p2是跟*结合,说明p2是指针,说明p2指向的是数组,每个元素是int类型。
数组名
数组名能表示首元素地址但是有二个意外
1。sizeof(数组名),单独放 这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2.取地址数组名 ,这里的数组名表示的是整个数组,取出的是整个数组的地址。因为&arr是取的整个数组,也是从arr[0]开始取的,他会一直取到这个数组完为止。+1跳过一个数组地址大小。
数组的地址也是从0开始,数组的首元素地址也是从0开始,只不过数组的地址+1,一次性跳过整个数组,是40个字节,而数组的首元素地址+1跳过的是4个字节。因为这个是int类型,有10个元素
分析 p是指向数组的,相当于指向整个数组的地址,也就是整个数组的地址,*p其实是整个数组,就相当于数组名,数组名代表整个数组,数组名又是首元素地址,所以*p本质上是数组的首元素地址。
访问元素是 *p+i就可以访问下标为i的元素的地址,解引用就可以访问下标为i个元素了
int*p=arr 说明 p指向的是数组名 数组名又是首元素地址 ,所以用指针来接收,使用加*号,说明p是指针,他指向的数组名的类型是int类型 说明是int类型
p2指向的是数组,是整个数组的地址所以用指针来接收,说明p2是指针,为了避免与[]结合要*号加括号括起来好一些,这个数组有10个元素,每个元素是int类型。
这个p指向了数组名的地址,这个数组有10个元素,每个元素的类型是int类型。它加上+i就可以访问下标为i元素的地址了,然后解引用就可以访问下标为
i的元素了。单独的*p就是找到了数组名
总结:
二维数组的数组名是第一行的地址也就是一维数组的地址,所以用一维数组指针来接受,然后
他一有三行,指向的是第一行 ,每一行有5个元素,所以指针指向的数组元素是5个,每个元素的类型是int类型,所以用int(*p)[5]来表示
(p+i)表示的是指向第i行的地址,也就是整行的地址,他+1表示跳过整个数组的大小也就是一维数组的地址,*(p+i)表示第i行的数组名也就是arr[i] 他+1表示跳过一个元素的大小。
如果是取地址第i行的数组名,+1就是跳过一行的地址大小。
二维数组的数组名相当于二维数组的首元素地址,这个首元素是第一行,也就是第一行的地址,第一行的地址相当于一维数组的地址 ,一维数组的地址需要一维数组的指针来存储。
所以需要指向一维数组的指针来接受。
p+i是指向i行的地址。,*(p+i)是指向第i行的数组名 相当于拿了第i行
数组名又是首元素地址,*(p+i)就是指向第i行的首元素地址,加上下标j就可以访问下标为j元素的地址,然后括起来解引用就可以访问第i行下标为j的元素了。
&arr的类型是int(*)[10]是个数组,是存放数组的指针,+1相对于跳过一个数组,所以是40个字节。
函数传参
int(*parr3)[10[5] ------> 首先parr3是一个数组,它有10个元素,每一一个元素的类型是int(*)[5] 说明parr3的每一个元素是指针,该指针指向的数组,也就是存了该数组的地址,该数组有5个元素,每个元素是int类型。
arr2是一个存放指针的数组,类型是int*, arr2是数组名,arr2就是首元素地址,存放的就是int*的地址, 一级指针的地址就是用二级指针来接受。
二级指针可以用一级指针的地址、二级指针来传参。
指针数组,里面存放的是指针,传指针数组的数组名,相当于传指针的地址,一级指针的地址就可以用二级指针来接受。
函数指针
指向函数的指针,取地址函数名就是函数指针。
函数名和取地址函数名都是函数的地址所以 函数名=&函数名
pf=&add----->(*pf)();
pf=add -----pf();
pf是指针变量,它指向函数,返回类型是int类型,参数也是int类型,*说明,pf是指针,*pf相当于找到了那个函数,pf存放的函数的地址。
int ret=pf(2,3)
int ret=add(2,3)这个*可以不用写。 为什么不写因为函数名和取地址函数名一样,pf是·个指针,它指向的是函数的地址,*pf就相当于找到了那个函数名
在这里说一遍,函数名和取地址函数名是一样的,因为函数没有首元素地址。
那就相当于pf=add
int add(int x, int y) { return x + y; } int main() { int a = 0; int b = 0; int(*pf) (int ,int ) = add; int red= pf(2, 3); printf("%d\n", red); return 0; }
其实就是对0强制类型转换,被解释为一个函数的地址。
首先void(*)()是一个函数指针类型,它把0强制类型转换成函数指针类型---->void(*)()0
然后对其解引用对其传参,其实这里的解引用可以不写,这个*相当于找到了这个函数,它你可以把(*void(*)()0)看成是一个函数指针变量,它的地址是0的地址 ,对它进行传参。,传参的参数为空---是一次函数调用,调用的是0地址处的函数 void (*pf)()
signal是一个函数名,它第一个参数是int类型,第二个参数是函数指针类型,它的返回值类型是一个void(*)(int)类型,也就是一个函数指针类型。
C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。
typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE
C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:
BYTE b1, b2;
可以简化成这个样子 typedef void(*pf_t)(int)
制作一个简单的计算器。
通过函数的指针来调用函数
void meau()
{
printf("*******************\n");
printf("****1.add 2.sub****\n");
printf("****3.mul 4.DIV****\n");
printf("****0.exit*********\n");
printf("*******************\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x *y;
}
int DIV (int x, int y)
{
return x / y;
}
void calc(int (*pf)(int, int))
{
int a = 0;
int b = 0;
int red = 0;
printf("请输入二个操作数\n");
scanf("%d %d", &a, &b);
red = pf(a, b);//pf里存的是add的地址,所以它调用到add函数里去了
printf("%d\n", red);
}
int main()
{
int input = 0;
do
{
meau();
printf("请选择\n");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 0:
printf("退出计算\n");
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(DIV);
break;
default:
printf("选择错误请重新选择\n");
break;
}
} while (input);
return 0;
}
回调函数就是通过函数的指针调用函数。
这个也是回调函数。回调函数就是通过a函数的指针调用a函数·,因为pf存的是add的地址,地址就是指针,就可以用pf这个指针来调用a函数。calc这个就是b函数,b函数里存放的是a的地址。
pf里存的是add的地址,所以它调用到add函数里去了
函数指针数组
这个arr就是函数指针的数组本质是数组,存放的函数指针,类型是int(*)(int,int)
找到arr[i]下标为i元素的函数就可以调用这个函数,这里有给误区这里的函数既是函数名又是函数地址,因为arr[]这个数组存的是函数名也就是函数的地址
找到数组arr下标为i的函数名,你可以解引用,也可以不解引用,这里的解引用没有特殊作用,因为函数名和&函数是一个作用,也就是通过arr的下标找到了这个函数然后来访问这个函数。
void meau()
{
printf("*******************\n");
printf("****1.add 2.sub****\n");
printf("****3.mul 4.DIV****\n");
printf("****0.exit*********\n");
printf("*******************\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x *y;
}
int DIV (int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int red = 0;
int (*parr[5])(int, int) = { NULL,add,sub,mul,DIV };
do
{
meau();
printf("请选择\n");
scanf("%d", &input);
if (input == 0)
{
printf("退出\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入二个操作数\n");
scanf("%d %d", &x, &y);
red = parr[input](x, y);
printf("%d\n", red);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
这个就是函数指针数组的好处。
指向函数指针数组的指针
首先pfarr是个指针,它指向数组,这个数组的类型是int(*)[5](int,int),是一个函数指针数组
回调函数
快排
cmp就是比较函数的地址 ,e1指向了你要比较的第一个元素,e2指向了你要比较的第二个元素
e1和e2是你要比较2个元素的地址。
通过用户自定义的函数比较出二个元素的大小传给cmp这个函数指针,分别对应的他二个参数,让cmp函数自己写出排序的顺序
第四个参数是比较待排序数据的2个元素函数
//快排
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
void*的指针不能直接解引用操作,void*是无类型指针,可以接受任何类型的地址。
void*是无类型指针,所以不能解引用操作,不能加1操作。
所以e1和e2要强制类型转换在解引用才能找到。
//比较结构体里的字符串大小 struct stu { char name[20]; int age; }; int cmp_stu(const void* e1, const void* e2) { strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);//结构体指针要用()起来。 } void test() { struct stu s [] = {{"zhangsan", 18}, {"lisi",20}}; int sz = sizeof(s) / sizeof(s[0]); qsort(s, sz, sizeof(s[0]), cmp_stu); } int main() { test(); return 0; }
模拟实现qsort函数
就是main函数里的cmp_int是一个函数对吧,不是指针,这个函数也就相当于这个函数的地址传给了这个cmp这个函数指针,这个函数指针调用cmp_int这个函数
1、cmp()会返回int型返回值;
2、cmp()带有两个指针型形式参数const void *
其中,const为一个限定词,含有const限定词的参数,尤其是以指针传递地址的参数,不会改变其原本的值。
void *,为一个无类型指针,既可以通过强制类型转换,使其转换为任意类型的指针,甚至直接取指向的数据值。
注意cmp这个不是库函数,但名字必须是cmp
cmp作为一个比较函数,可以简单方便的对bsearch、qsort等函数提供交换依据。
它的基本形式为:
用cmp指向的那个函数来比 二个参数是二个元素的地址
首先base是你要排序的起始地址 首先要转换成char*类型,为什么要转换成char*类型,因为char*类型+多少字节方便,然后+上元素的个数就是j,当j等于0的时候也就是从起始地址开始,最后乘以宽度,这个宽度就是待排序的数据元素的大小,单位是字节,比如我们排int类型的数据就是乘以4
起始地址+偏移量的方法来计算
偏移量计算:=下标*宽度(元素的宽度啊)
知道你要交换的起始地址还不够,还必须知道宽度 就是这二个元素的起始地址每个地址占的宽度。
通过循环和逐个字节交换的方式:
因为我们无法直接知道传入的数据的具体类型,所以只能通过按字节逐个交换的方式来完成内存块的交换。循环的次数为 Width
,即内存块的大小,这样可以确保我们对整个内存块进行遍历和交换。
循环的次数为 Width
是因为这段代码是用来交换内存块内容的通用函数,而不关心数据的具体类型。在这种情况下,我们必须以字节为单位进行操作,因为无法确定传入的数据的具体类型。
假设我们有一个包含 4 个整型元素的数组,每个整型元素占据 4 个字节。如果我们要交换两个元素,就需要将每个字节都进行交换。因此,循环的次数必须等于内存块的大小,以确保每个字节都被正确地交换。
另外,通过循环的方式可以确保在不同平台上也能够正确地执行内存块的交换操作,不受数据类型或内存对齐方式的影响。这样的通用性使得这个交换函数可以适用于各种不同类型和大小的内存块,从而提高了代码的重用性和灵活性
int cmp_int(const void *e1,const void* e2)
{
return *(int*)e2 - *(int*)e1;
}
void Swap(char* e1,char *e2,int width)
{
for (int i = 0; i < width; i++)
{
char tep = *e1;
*e1 = *e2;
*e2 = tep;
e1++;
e2++;
}
}
void bubble_short(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
//排序
//cmp是你要比较的元素地址
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
//比较的元素地址 和每个元素的字节大小宽度
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_short(arr, sz, sizeof(arr[0]), cmp_int);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
简单制作求最大值和求最小值的方法
int max_1 (int a, int b)
{
return a > b ? a : b;
}
int min_1(int a, int b)
{
return (a < b) ? a : b;
}
int main()
{
int a = 0;
int b = 0;
int input = 0;
printf("输入1是求最大值,输入2是求最小值\n");
scanf("%d", &input);
printf("输入二个操作数\n");
scanf("%d %d", &a, &b);
int (*pf[3])(int, int) = { NULL,max_1,min_1 };
if (input >= 1 && input <= 2)
{
int red = pf[input](a, b);
printf("%d\n", red);
}
return 0;
}
指针的笔试题目
//深度讨论数组名 int main1() { int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a)); //sizeof内部出现数组名代表是整个数组的大小 //16 printf("%d\n", sizeof(a + 0)); //首元素地址+0还是首元素地址 4/8 printf("%d\n", sizeof(*a)); //首元素地址进行解引用相当于a[0]是4个字节 //*a中的a是数组首元素的地址,* a就是对首元素的地址解引用,找到的就是首元素 //首元素的大小就是4个字节 printf("%d\n", sizeof(a + 1)); //首元素地址+1 相当于a[1]的地址 是地址就是4/8 printf("%d\n", sizeof(a[1])); //第二个元素大小//4 printf("%d\n", sizeof(&a)); //取地址数组名,取的是整个数组的地址,是地址就是4/8 printf("%d\n", sizeof(*&a)); //&a----->的类型是int(*)[4] &a拿的是数组名的地址,数组指针解引用拿到了这个数组是 //是数组的大小就是16 printf("%d\n", sizeof(&a + 1)); //取数组名+1相当于跳了一个数组的大小,是地址就是4/8 printf("%d\n", sizeof(&a[0])); //取第1个元素的地址 是地址就是4/8 printf("%d\n", sizeof(&a[0] + 1)); //取第2个元素的地址 是地址就是4/8 return 0; } int main2() { char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", sizeof(arr));//sizeof内部出现数组名代表是整个数组的大小 //6 printf("%d\n", sizeof(arr + 0));//首元素'a'的地址---4/8 printf("%d\n", sizeof(*arr)); //'a'的大小/1 printf("%d\n", sizeof(arr[1]));//'a'的大小/1 printf("%d\n", sizeof(&arr));//取出整个数组的地址是地址就是4/8 printf("%d\n", sizeof(&arr + 1));//跳过一个数组的地址,是地址就是4/8 printf("%d\n", sizeof(&arr[0] + 1));//'b'的地址是地址就是4/8 return 0; } #include <string.h> int main3() { char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", strlen(arr));//随机值 因为不知道编译器在哪里放\0 printf("%d\n", strlen(arr + 0)); //随机值 printf("%d\n", strlen(*arr));//相当于strlen('a')strlen(地址)//err 是一个野指针 printf("%d\n", strlen(arr[1]));//野指针 printf("%d\n", strlen(&arr));//取数组的地址,数组的地址也是从‘a'的地址开始的 随机值 printf("%d\n", strlen(&arr + 1));//取数组名+1跳过了一个数组的地址, 随机值-6 printf("%d\n", strlen(&arr[0] + 1)); // 首元素的地址+1相当于 是字符b的地址开始 随机值-1 return 0; } int main4() { char arr[] = "abcdef";//[a b c d e f\0] printf("%d\n", sizeof(arr)); //计算的是整个数组的大小,7 printf("%d\n", sizeof(arr + 0));//首元素地址 相当于是‘a'的地址 是地址就是4/8 printf("%d\n", sizeof(*arr));//’a'元素的大小 1 printf("%d\n", sizeof(arr[1]));//1 printf("%d\n", sizeof(&arr));//取数组的地址 也是从‘a'的地址开始的 也是 4/8 printf("%d\n", sizeof(&arr + 1));//跳了一个数组的地址是地址就是 4/8 printf("%d\n", sizeof(&arr[0] + 1)); //相当于是’b'的地址 4/8 return 0; } //strlen是求字符串长度的,关注的是字符串中的\0,计算的是\0之前出现的字符的个数 // //strlen是库函数,只针对字符串 // //sizeof只关注占用内存空间的大小,不在乎内存中放的是什么 // //sizeof是操作符 int main5() { char arr[] = "abcdef";//[a b c d e f\0] printf("%d\n", strlen(arr));//6 printf("%d\n", strlen(arr + 0));//6 //printf("%d\n", strlen(*arr));//err //printf("%d\n", strlen(arr[1]));//err printf("%d\n", strlen(&arr));//‘a’字符的地址开始 6 printf("%d\n", strlen(&arr + 1)); //跳过了\0 从\0后面开始 随机值 printf("%d\n", strlen(&arr[0] + 1)); //从'b'字符开始走 5 return 0; } int main() { char* p = "abcdef";//[a b c d e f\0] printf("%d\n", sizeof(p));//p指向的是字符a的地址 4/8 printf("%d\n", sizeof(p + 1)); //指向的是字符b的地址 4/8 //printf("%d\n", sizeof(*p)); //err //printf("%d\n", sizeof(p[0])); //err printf("%d\n", sizeof(&p));//取出的是一级指针的地址 4/8 printf("%d\n", sizeof(&p + 1)); //4/8 printf("%d\n", sizeof(&p[0] + 1)); //4/8 printf("%d\n", strlen(p)); //p指向的是a的地址 6 printf("%d\n", strlen(p + 1)); //p指向的是b的地址 5 //printf("%d\n", strlen(*p)); //P找到了a的这个字符 err ///printf("%d\n", strlen(p[0])); //err printf("%d\n", strlen(&p)); //取出的是一级指针的地址 随机 printf("%d\n", strlen(&p + 1));//随机 printf("%d\n", strlen(&p[0] + 1));//5 //P+1 p指向的是b的地址 return 0; }
为什么strlen(&P)是随机值是因为指向p的那块空间上的地址存储方式不确定,有可能是大端存储,也可能是小段存储
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//48
printf("%d\n", sizeof(a[0][0]));//4
printf("%d\n", sizeof(a[0]));a[0]是第一行这个一维数组的数组名,单独放在sizeof内部,a[0]表示第一个整个这个一维数组
//sizeof(a[0])计算的就是第一行的大小 也就是第一行的数组名 16
printf("%d\n", sizeof(a[0] + 1)); //没有单独放在sizeof内部,表示的是首元素地址
//也就是第一行这个一维数组的第一个元素的地址--a[0][0]的地址 然后+1 就是第一行第二个元素的地址 4/8
printf("%d\n", sizeof(*(a[0] + 1))); //第一行第二个元素的地址进行解引用 也就是arr[0][1]元素的大小 --4
printf("%d\n", sizeof(a + 1));// 第一行的地址+1 相当于跳到了第二行的地址 是地址就是4/8
printf("%d\n", sizeof(*(a + 1))); //第二行的地址解引用 ---就相当于是第二行的数组名 16
printf("%d\n", sizeof(&a[0] + 1));// 对第一行的数组名取地址 相当于是第一行的地址,第一行的地址+1相当于是第二行的地址 4/8
printf("%d\n", sizeof(*(&a[0] + 1))); //第二行的地址解引用相当于拿到了第二行的数组名 16
printf("%d\n", sizeof(*a)); //第一行地址解引用 拿到了第一行的数组名 16
printf("%d\n", sizeof(a[3]));//第三行的数组名 虽然越界了 但是不影响计算它内部的大小 16
return 0;
}
1.题目1
取数组的地址说明这个指针是的类型是int(*)[5]的,说明这个指针是数组指针,指针指向这个数组,数组有5个元素,每个元素的类型是int类型
题目的指针是int*类型,说明我们要强制类型转换成int*类型的
2.题目2![cb01e0fd326b47148880ebc2fae25355.png](https://img-blog.csdnimg.cn/cb01e0fd326b47148880ebc2fae25355.png)
这里的p是一个结构体指针
1.结构体指针+1相当于跳过的是一个结构体的大小 已经知道一个结构体的大小是20个字节就相当于跳了20个字节。0x100000+20----ox1000014
2.把p强制类型转换成无符号长整形,,已经知道1,048,576是p转换成无符号长整形的数据,它+1= 048,577
3.把p转换成无符号整形指针类型 +1就相当于跳过一个无符号整形 相当于+4 = 1,048,576+4
3.题目3
取数组的地址说明这个指针是的类型是int(*)[4]的,说明这个指针ptr1是数组指针,指针指向这个数组,数组有4个元素,每个元素的类型是int类型
题目的指针是int*类型,说明我们要强制类型转换成int*类型的
ptr1[-1]是指向的是04 00 00 这块区域的 同理也是00是高地址的 04是低地址的拿出来的时候就是低地址在后面 00 00 00 04 以16进制打印的时候前面的0全部删除掉 就是4了
ptr2: 首先这个a是数组名,数组名是首元素地址,首元素地址+1是跳过一个整形也就是4个字节,题目强制类型转换成了int类型说明现在的a不是地址了是一个整数,它+1就说明加了一个整数
比如1的16进制是 00 00 00 01
在内存是小端存储模式,01是低地址处,----01 00 00 00
a+1强制类型转换成int*类型的指针,所以ptr2就指向了 00 00 00 02这块起始地址。因为 00 00 00 02在内存中是以小端模式存储,拿出来的时候就是02 00 00 00 以16进制打印的时候就是200 00 00
因为·02是高位的地址就应该放在低地址处
4.题目4
首先这个二维数组的元素是以逗号表达式的形式展开,你要明确这个数组里的元素是{1,3,5,0,0,0}p指向了第一行的数组名,此时的数组名表示的一行的地址,数组名没有放在sizeof内部,也没有取地址,说明数组名表示首元素地址 也就是a[0][0]的地址然后解引用就是1了
5.题目5
这里的a作为数组名,它是第一行的地址,它的类型是int(*)[5] ,因为二维数组的数组名代表的是第一行的地址,也就是第一行数组名的地址 一维数组名的地址就需要用一维数组的指针来接受。
这里的p是一个数组指针,它指向的是一个数组,这个数组有4个元素,每个元素的类型是int类型
这里把二维数组的数组名赋给数组指针 类型存在差异,但不影响,只不过是报警告
再来看看 p是一个数组指针 它+1跳过一个数组的大小,这个数组有4个元素,每个元素的类型是int类型,相当于跳了4个整形的大小,p[4][2]可以看成 *( *(p+4)+2)
*(p+4)这个相当于p跳了4个整形的大小 再图可以看出 它跳到了a[3][1]的 地址,然后解引用找到了arr[3]的数组名,就可以访问相当于arr[3][1]~ arr[3][4]这块区域的元素的地址,然后+2跳过二个整形长度指向了arr[3][3]这块地址然后解引用找到了arr[3][3] 这个元素
我们是把p[4][2]看成arr[3][3] ,指针-指针的绝对值是元素之间的个数,他们相差了4个 又因为地址再内存中是从低到高存放的,所以结果是-4
打印地址是打印它在内存中的地址,所以这里不考虑大小端字节序,也不考虑原码补码反码,
再x86的环境下就是FF FF FC 记住打印内存是打印反码的形式打印内存
6.题目6
7.题目7
pa是指向的是a的起始地址
pa++跳到了第二行,对pa解引用找到了a的地址相当于指向了a的地址
pa指向了a,a是一个指针数组pa指向的对象是一个char*类型的,第二*说明pa是一个指针
pa++=pa+1 自然是跳过一个char*类型 也就是4个字节。
int*p 是一个指针,它指向的对象是int类型,所以+跳过了4个字节。
8.题目8
+
结构体详解
这个就是结构体的创建和初始化。
printf(“%s %d %s %s”,s.name,s.age,s.sex,s.tele) 结构体对象.成员名
可以把结构体和指针结合起来玩
ps箭头指向的s的成员,也是解引用效果。
结构体指针变量->成员名(得到了地址(指针))。
结构体对象.成员名
分支语句
if语句
else离最近的if语句结合。
if语句题目
//1. 判断一个数是否为奇数
//2. 输出1 - 100之间的奇数
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
if (n % 2)
{
printf("奇数\n");
}
else
{
printf("不是奇数\n");
}
return 0;
}
#include <stdio.h>
int main()
{
int i = 0;
for (i = 0; i <= 100; i++)
{
if (i % 2)
{
printf("奇数%d\n",i);
}
}
return 0;
}
int main()
{
int i = 0;
for (i = 1; i <= 100; i+=2)
{
printf("奇数%d\n",i);
}
return 0;
}
Switch语句题目
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期天\n");
break;
default:
printf("输入错误\n");
break;
}
这里的每条case后面必须加break语句否则如下图所示。
break会跳出switch语句。
switch语句的表达式必须是整数。
题目解释
#include <stdio.h>
int main()
{
int n = 1;
int m = 2;
switch (n)
{
case 1:
m++;//进入这里m=3,没有break执行下一个
case 2:
n++;//n=2 没有break执行下一个
case 3:
switch (n)n=2
{//switch允许嵌套使用
case 1:
n++;
case 2:
m++;m=4
n++;n=3
break;
}
case 4:
m++;m=5
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
循环语句
while循环
先看表达式是否为真,为真就执行表达式结果,然后继续看表达式是否为真,一直反复,知道表达式不满足,就跳出循环。
比如打印1-10的数
int main()
{
/*int i = 1;
while (i<11)
{
printf("%d ", i);
i++;
}*/
return 0;
}
break和continue的用法和区别
因为这里的i++在后面,没有执行。
break是跳出循环的,continue后面的语句不会执行。所以一直进入死循环
区别:break用于循环的永久终止
continue是跳出本次循环后面的代码,直接进入判断部分,进入下一次循环。
可以这么改
几个常见的陷阱
这里的\n也是占一个字符
for循环
先初始化,在判断,执行循环语句,然后调整,直到判断不成立,就跳出循环。
for循环遇见break和continue
与while一样
continue的后面的语句 不会执行,但是for循环的i++在前面。,他跳到了调整部分去了
循环里的语句不要随便乱省阅
这里第二就是i=0的时候内层循环执行三次,此刻i=1,但是j'现在等于3,内层循环没有初始化所以就打印了三次。
for循环题目
这里=是赋值,表达式2结果是假的,就执行一次。
、
do-while循环语句
先执行循环语句,在判断表达式,如果表达式为真,就继续,否则跳出循环。
几个循环题目
1. 计算 n的阶乘。
2. 计算 1!+2!+3!+……+10!
3. 在一个有序数组中查找具体的某个数字n。(讲解二分查找)
4. 编写代码,演示多个字符从两端移动,向中间汇聚。
//1. 计算 n的阶乘。
int main()
{
int i = 0;
int n = 0;
int ret = 1;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n", ret);
return 0;
}
//2. 计算 1!+ 2!+ 3!+ …… + 10!
int main()
{
int i = 0;
int n = 0;
int ret = 1;
int sum = 0;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
ret *= i;
sum += ret;
}
printf("%d\n", sum);
return 0;
}
二分查找算法
首先把arr[0]为left arr[9]为right 中间元素为mid 先判断arr[mid]>或者 <你要找的元素的值 ,运气好的话中间的元素就是你要查找的值。如果不是你要找的值,就判断arr[mid】>k就right=mid-1 ,<就left=mid+1
比如找元素7,他的下标为6,创建一个变量left,含义是起始的,right,是末端的,中间变量是mid(left+right)/2,left=mid+1,right=mid-1.比如arr[mid]<k,中间元素是下标是4,找的元素是下表是6,在后面,则是left=mid+1,right还是right。现在left下标是5,right下标是9,中间下标是7,找的下标是6,此时right变成了mid-1,就剩下二个元素了,就是6和7 下标是5和6 ,left=5,right=6,中间是5,,mid=5 此时必要要找的下标元素小,那就是left=mid+1就找到了。
#define _CRT_SECURE_NO_WARNINGS
//3. 在一个有序数组中查找具体的某个数字n。(讲解二分查找)
#include <stdio.h>
#include <string.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left+(right-left) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
printf("找到了下标是%d\n", mid);
break;
}
}
if (left > right)
{
printf("找不到\n");
}
return 0;
}
明确区别sizeof 和strlen()
//4. 编写代码,演示多个字符从两端移动,向中间汇聚
int main()
{
char arr1[] = "abcrerewf";
char arr2[] = "*********";
int left = 0;
int right = strlen(arr1) - 1;
while (left<=right)
{
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
Sleep(1000);
left++;
right--;
}
return 0;
}
这里的Sleep是时间戳头文件是<windows.h>文件,就是起延时作用
system("cls"),头文件是<stdlib.h>,是一个清空屏幕的效果。
//5. 编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则
//提示登录成,如果三次均输入错误,则退出程序。4
int main()
{
char password[20] = { 0 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("请输入密码:");
scanf("%s", password);//假设密码是123;
if (strcmp(password, "123") == 0)
{
printf("登录成功\n");
break;
}
else
{
printf("密码错误\n");
}
}
if (i == 3)
{
printf("三次全部错误退出程序\n");
}
return 0;
}
猜数字游戏
猜数字游戏
1.电脑产生一个随机数
2.猜数字
3.猜大了
4.猜小了
5.直到猜对了,结束。
time(NULL)获得时间戳。头文件是<time.h>
rand()生成随机数的库函数
调用rand()之前必须调用srand(填一个随机数),头文件是<stdlib.>
生成随机数srand(usigned int ) time(NULL),这个就是生成随机数
但是这个只能调用一次。
//猜数字游戏实现
void game()
{
int n = 0;
int red =rand()%100+1;
//2猜数字
while (1)
{
printf("请猜数字\n");
scanf("%d", &n);
if (n < red)
{
printf("猜小了\n");
}
else if(n > red)
{
printf("猜大了\n");
}
else
{
printf("猜对了\n");
break;
}
}
}
void meau()
{
printf("*********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
}
int main()
{
int input = 0;
do
{
srand((unsigned)time(NULL));//产生随机数
meau();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
// 猜数字
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误重新选择\n");
break;
}
} while (input);
return 0;
}
关机程序
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>//strcmp()函数的头文件
int main()
{
char input[20] = { 0 };
system("shutdown -s -t 120");// 必须写成1这个样子
while (1)
{
printf("请注意,你的电脑在120秒内关机,如果输入:我是你爹,就取消关机\n");
scanf("%s", input);
if (strcmp(input, "爹") == 0)
{
system("shutdown -a");
break;
}
}
return 0;
}
函数的详解 ![287dffb7eeab42778a8718c39bab0ef7.png](https://img-blog.csdnimg.cn/287dffb7eeab42778a8718c39bab0ef7.png)
当实参传递给形参的时候,形参是实参的一份临时拷贝。
对形参的修改不会影响实参。
只有通过指针来存储地址,指针来进行修改实参的值
函数题目
素数,只能被1和他本身整数的数叫素数
比如7 只能被1和7整除的,拿2~(i-1)的数来试,如果发现2到(i-1)有数字来整除j,说明i不是素数。
没必要从2到i-1之前来判断是不是素数,只需要判断sqrt(m)里的数是不是素数。
写一个函数可以判断一个数是不是素数。
打印100~200之间的素数
素数是只能被1和他本身整除的数
7
1和7整除
2 3 4 5 6
int main()
{
int i = 0;
int count = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数
//是素数就打印
//拿2~i-1之间的数字去试除i
int flag = 1;//flag是1,表示是素数
int j = 0;
for (j = 2; j <= i - 1; j++)
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count++;
printf("%d ", i);
}
}
printf("\ncount = %d\n", count);
return 0;
}
#include <math.h>
sqrt是数学库函数
开平方
math.h
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i+=2)
{
//判断i是否为素数
//是素数就打印
//拿2~i-1之间的数字去试除i
int flag = 1;//flag是1,表示是素数
int j = 0;
for (j = 2; j <= sqrt(i); j++)
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count++;
printf("%d ", i);
}
}
printf("\ncount = %d\n", count);
return 0;
}
写一个函数可以判断一个数是不是素数。
是素数返回1
不是素数返回0
int is_prime(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i+=2)
{
//判断i是否为素数
//是素数就打印
//拿2~i-1之间的数字去试除i
if (is_prime(i))
{
printf("%d ", i);
count++;
}
}
printf("\ncount = %d\n", count);
return 0;
}
//2. 写一个函数判断一年是不是闰年。
//能被4整除并且不能被100整除的是闰年或者被400整除的是闰年
//1000-20000
int main()
{
int i = 0;
for (i = 1000; i <=2000; i++)
{
if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0)
{
printf("%d ", i);
}
}
printf("\n");
return 0;
}
int is_leap_year(int i)
{
if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int i = 0;
for (i = 1000; i <=2000; i++)
{
if (is_leap_year(i))
{
printf("%d ", i);
}
}
printf("\n");
return 0;
}
//二分查找
int cz(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5 ,6,7,8};
int k = 3;
int sz = sizeof(arr) / sizeof(arr[0]);
int j = 0;
int ret=cz(arr, k, sz);
if (ret == -1)
{
printf("找不到\n");
}
else
{
printf("找到了下标是%d\n", ret);
}
return 0;
}
不要在函数内部求参数元素的大小
//4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void add(int* p)
{
(*p)++;
}
int main()
{
int n = 0;
add(&n);
printf("%d\n", n);
add(&n);
printf("%d\n", n);
return 0;
}
函数的嵌套
链式访问。
printf()函数返回值看字符个数,首先看最内层的函数,打印43,有二个字符,返回2,一个字符返回1
虽然默认是int类型,但是不能这样做。非常不好
函数的递归
这个是倒置的
讲解递归方法
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
这里的arr传的是首元素地址,地址就是用指针来接受,来存储,*p解引用来找到元素一直自加,直到找到\0才结束。
这里的my_strlen(str+1),str是字符a地址,加一是字符b的地址 1+bc的长度
//实习自定义求字符串长度函数
int my_strlen(char* p)
{
if (*p != '\0')
{
return 1 + my_strlen(p + 1);
}
else
return 0;
}
int main()
{
char arr[] = "abc";//[a b c\0]
int ret=my_strlen(arr);
printf("%d\n", ret);
return 0;
}
//例如,调用DigitSum(1729),则应该返回1+7+2+9,它的和是19
int DigitSum(int n)
{
if (n > 9)
return DigitSum(n / 10) + n % 10;
else
return 1;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = DigitSum(n);
printf("%d ", ret);
return 0;
}
函数的迭代与递归
1!=1,2!=2*1,3!=3*2*1...................
//利用递归求n的阶乘
int dg(int n)
{
if (n <= 1)
{
return 1;
}
return n * dg(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret=dg(n);
printf("%d\n", ret);
return 0;
}
函数栈帧
每一次函数调用,都会向内存栈区申请一块空间,这一块空间主要是用来存放函数中的局部变量,和函数调用过程的上下文的信息,这个一块空间一般叫:函数的运行时堆栈,也叫函数栈帧空间编译会自动根据需要开辟空间的。
斐波拉契数
//斐波那契数
//前二个相加等于后面一个
int fib(int n)
{
if (n <= 2)
{
return 1;
}
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d", ret);
return 0;
}
//斐波那契数
//前二个相加等于后面一个
1 1 2 3 5 8 13 21 34 ......
产生新的a和b
//斐波那契数
//前二个相加等于后面一个
int fib(int n)
{
int a = 1;
int b = 1;
int c = 0;
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d", ret);
return 0;
}
辗转相除法
就是把a整除b的结果不等于0,就把c赋给b,b赋值给a,直到整除结果等于0,那个除的就是最大公约数。
最小公倍数就是二个数相乘除以最大公约数。
int main()
{
int a = 0;
int b = 0;
int c = 0;
scanf("%d %d", &a, &b);
while (a % b) {
c = a % b;
a = b;
b = c;
}
printf("最大公约数%d\n", b);
int ret = a * b / b;
printf("最小公倍数%d\n", ret);
return 0;
}
//计算1 / 1 - 1 / 2 + 1 / 3 - 1 / 4 + 1 / 5 …… + 1 / 99 - 1 / 100 的值,打印出结果
//分子总是1,分母是1-100
int main()
{
int i = 0;
double sum = 0;
int flag = 1;
for (i = 1; i <= 100; i++)
{
sum += flag * (1.0 / i);
flag = -flag;
}
printf("%lf", sum);
return 0;
}
99乘法表
数组的讲解
【】下标引用操作符号。
int类型地址相差4个字节。
数组在内存是连续存放的。
二维数组不能省约列
行和列的下标都是从0开始的
可以把二维数组理解为一维数组的数组
可以把arr【0】看成第一行的数组名。数组名家【j】可以访问这一行的某个元素。
二维数组在数组中也是连续存放的
越界了。
数组传参只传数组名。
数组名
数组名是首元素地址。
取地址数组名 也是从首元素地址开始取的。
纠正
数组名能表示首元素地址但是有二个意外
1。sizeof(数组名)这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2.取地址数组名 ,这里的数组名表示的是整个数组,取出的是整个数组的地址。因为&arr是取的整个数组,也是从arr[0]开始取的,他会一直取到这个数组完为止。
除了这二个以外,其他的数组名就是首元素地址。
取地址数组+1跳过的整个个数组的。
二维数组的数组名理解
二维数组的数组名表示的是第一行的地址。
我们要把二维数组想象成一维数组,相对于图中的有三行,就相当于有3个元素。
1行一个元素。那二维数组名表示首元素地址,就是第一行的地址,也就是第一个元素的地址。就是这个一维数组的地址。数组名表示这一行的地址,这一行也就是一维数组的地址。不是第一行第一个元素的地址。
arr+1表示第二行地址。
sizeof()arr/arr[0] 这个计算的是行数
sizeof() arr[0]/arr[0][0] 一行的大小除以第一行第一个元素的大小就是求列数
int main()
{
int arr[3][4] = { 0 };
printf("%d\n", sizeof(arr) / sizeof(arr[0]));//3 计算的是行数
//这个sizeof(arr)计算的是整个数组的大小是48,sizeof(arr[0]) ,arr[0]是第一行的数组名
//第一行的数组名 放在sizeof内部,计算的是这一行的大小是16 ,因为一行有4个元素
printf("%d\n", sizeof(arr[0]) / sizeof(arr[0][0]));//4 计算的是列数
//sizeof(arr[0])计算的是一行的大小,是16,sizeof(arr[0][0])计算的是第一行第一个元素的大小是4。
return 0;
}
。
记住第一行的数组名是个地址,用sizeof内部起来就是一个字节大小。除了那二个特例看到数组名就是首元素地址,地址就是指针,记住,二维数组的数组名也是地址,只不过是第一行的地址。就是一维数组的地址。
可以把arr【0】看成第一行的数组名。
arr是二维数组的话,&arr是取二维数组的地址。
冒泡排序
冒泡的核心就是n个元素要比n-1趟,并且每一趟的元素在减少。比如10个元素要比9趟,第一趟比9趟,第二次比8趟,元素也从10个变成9个。
//冒泡排序
void bubble_sort(int arr[], int sz)
{
int i = 0;
//10个元素,10-1趟
for (i = 0; i < sz - 1; i++)//
{
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])//交换
{
int tep = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tep;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
arr[j]>arr[j+1]这个是升序,把大于改成小于就是降序
三子棋程序
字符串逆序
递归版本
就是先把a与f交换在逆序bcde,然后在交换b与e,逆序cd
迭代版本
//字符逆序
void reveser(char arr[])
{
int right = strlen(arr) - 1;//下标是从0开始的
int left = 0;
while (left < right)
{
int tep = arr[left];
arr[left] = arr[right];
arr[right] = tep;
left++;
right--;
}
}
int main()
{
char arr[] = "abcdefg";
reveser(arr);
printf("%s", arr);
return 0;
}
核心就是交换。
计算一个数的每位之和
//写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
//例如,调用DigitSum(1729),则应该返回1 + 7 + 2 + 9,它的和是19
//输入:1729,输出:19
int print(int n)
{
if (n > 9) {
return print(n / 10) + n % 10;//123 4
}
else
{
return n;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum=print(n);
printf("%d\n", sum);
return 0;
}
//编写一个函数实现n的k次方,使用递归实现
n的k次方相当 于n*n的(k-1)次方
//编写一个函数实现n的k次方,使用递归实现
double pow(int a, int b)
{
if (b > 0)
{
return a * pow(a, b - 1);
}
else if (b == 0)
{
return 1;
}
else
{
return 1.0 / pow(a, -b);
}
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
double ret = pow(a, b);
printf("%lf", ret);
return 0;
}
交换数组
//创建一个整形数组,完成对数组的操作 //实现函数init() 初始化数组为全0 //实现print() 打印数组的每个元素 //实现reverse() 函数完成数组元素的逆置。 //要求:自己设计以上函数的参数,返回值 void reverse(int arr[], int sz) { int left = 0; int right = sz - 1; while (left < right) { int tep = arr[left]; arr[left] = arr[right]; arr[right] = tep; left++; right--; } } void print(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } void init(int arr1[], int sz) { int i = 0; for (i = 0; i < sz; i++) { arr1[i] = 0; } } int main() { int arr1[] = { 1,2,3,4,5 }; int sz = sizeof(arr1) / sizeof(arr1[0]); print(arr1, sz); reverse(arr1, sz); print(arr1, sz); init(arr1, sz); print(arr1, sz); return 0; }
操作符详解
正数的原码反码补码相同
负数的原码最高位数是1,正数为0
整数在内存中存储的是补码
负数的左移与右移,移的是补码,打印的是源码
补码-1取反就是原码。
左移有乘2的效果
左移和右移只针对整数。
vs里的右移操作赋采用的是算数右移,右边丢弃、左边补原符号位
符号位是根据正数还是负数来确定的,正数补0,负数补1.
总结:
计算的是以补码形式计算,打印的是以原码形式存在。
计算的时候要用补码,因为整数在内存中存储的是补码。
按位与二个同时为1就得1,有一个不得1就为0.
取反的时候不要动符号位。
异或是相同为0,相异为1
0异或a是a a异或a是0
异或支持交换律
统计二进制中有多少个1
任何一个数a按位与1如果==1就说明a的二进制最低位是1
a&1==0 说明a的二进制最低位是0
就比如这段二进制代码 a&1 第1位是0 就&1 得0 就跳过最低位,看第二位,如果第二位&1等于1就让COUNT++,一直循环下去。这样就可以知道这段二进制代码有多少个1了。
方法1 利用&和>>操作符计算出二进制代码有多少个1.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int i = 0;
int a = 0;
while (~scanf("%d", &a))
{
int count = 0;
for (i = 0; i < 32; i++)
{
if ((a >> i) & 1 == 1)
{
count++;
}
}
printf("%d\n", count);
}
return 0;
}
总结
把一个二进制向右移动i个位移到最低位和1进行按位与进行比,如果==1就说明二进制有1
从右往左看,每次移到的位位数再增加,最高是32位
方法2:利用/2 %2的方法来
、
想得到二进制的每一位 /2 %2来解决这个问题
//考虑正负数问题
int count_one_bit(unsigned int n)
{
int count = 0;
while (n)
{
if ((n % 2) == 1)
{
count++;
}
n /= 2;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret=count_one_bit(n);
printf("%d\n", ret);
return 0;
}
方法3
利用n=n&(n-1)来达到把n的二进制最右边的1去掉
比如n=-1
1111 n
1110 n-1
1110 n
1101 n-1
1100 n
1011 n-1
1000 n
0111 n-1
0000 n
利用这种效果来实现二进制代码有多少个1
//判断一个数是否是2的次方数
//判断一个数的是否是2的次方数
//n=n&(n-1)
// 0001
//0010
//0100
//1000
int main()
{
int n = 0;
scanf("%d", &n);
if ((n & (n - 1)) == 0)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
return 0;
}
练习2 二进制位 置0或者置 1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0?
//编写代码将13⼆进制序列的第5位修改为1,然后再改回0 ?
// 0001 1101
int main()
{
int n = 0;
scanf("%d", &n); // 13 0000 1101
//移到
n = n | (1 << 4); // 0000 1101 | 0001 0000==0001 1101
printf("%d\n", n);
//复原
// 0001 1101 变成 0000 1101
// 按位与1110 1111 即可 怎么变呢 1的二进制是 0000 0001 左移4位 0001 0000然后取反 1110 1111
n = n & ~(1 << 4);
printf("%d\n", n);
return 0;
}
获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
总结方法:
输入的数只要往右移,把每一个数移到最低位按位与上一个1就可以获得最低的是否是1了,可以明确判断出你输入的数转换成二进制形式有多少个1.
编程实现:两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同
语法说说支持连续赋值,建议写代码不要这么写。
效果一样。
强制类型转换。
1是40 2是4/8,3是10,4是4/8
逻辑与,左边为假右边就不计算了。
逻辑或,左边为真,右边就不计算了
逗号表达式是从左到右计算,整个表达式的结果是最后一个表达式的结果。
整型提升
一个char类型的占1个bit 相对于8个biye位,他要整型提升是因为 难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
例如图中的a为5,b为126 先把a的原码写出来相加得出来后8位,首先看最高位是0或者1,是1补1,是0补0,得出来的是补码,因为整数在内存中的存储是补码形式的,然后-1取反得原码,因为打印的都是以原码形式打印的。
a与b会发生整形提升,a与b的值会发生变化。
无符号的数发生整形提升,高位补0
上面的这些大小都小于int类型
下面讨论大小大于或者等于int类型的整形提升
这些类型都是向上转换的。
数据存储
有符号数直接用int或者 signed int类型的
无符号直接用unsigned int 类型的
无符号不分最高位是正数还是负数,统一正数。
整形在内存中以补码形式存储
1+-1是通过补码形式实现+-的
原码取反+1得补码
补码取反+1得原码
把一个数的高位字节序内容放在低地址处,把一个低位字节序内容放在高地址处。这个叫大端存储
把一个数的高位字节序内容放在高地址处,把一个低位字节序内容放在地地址处 这个叫小端存储
放与拿相反。比如放进去的是44 33 22 11,拿出去的是11 22 33 44
vs里存的是小端存储模式
主要看起始地址,对起始地址解引用看是不是1还是0,并且还要强制类型转换成char*类型,int类型一次性访问4个字节。
由于c是无符号数,整形提升的时候最高位是1,由于是无符号数,前面直接补0,所以是255,正数的原码补码,反码一样。
字符串函数
1.strlen函数 求字符串长度的
//求字符串长度 int my_strlen(char* str) { int count = 0; while (*str != '\0') { count++; *str++; } return count; } int main() { char arr[] = "abcdef"; int red = my_strlen(arr); printf("%d\n", red); return 0; }
2. strcpy 字符串拷贝
//求字符串拷贝 char* my_strcpy(char* arr2, char* arr1) { char *red = arr2; while (*arr2++ = *arr1++)//'\0'赋给arr2时arr2为假就停止循环 { ; } return red; } int main() { char arr1[] = { "abc" }; char arr2[40] = { 0 }; my_strcpy(arr2, arr1); printf("%s", arr2); return 0; }
3、strcat 字符串连接
先找到需要连接的字符串末端'\0'处,然后拷贝
拷贝是在\0这里进行拷贝。
//字符串连接 char* my_strcat(char* arr1, char* arr2) { char* ret=arr1; while (*arr1 != '\0') { arr1++; } while (*arr1++ = *arr2++) { ; } return ret; } int main() { char arr1[40] = { "hello" }; char arr2[] = "bit"; my_strcat(arr1, arr2); printf("%s", arr1); return 0; }
4.strcmp-字符串比较函数。
因为这个是二个不同的数组名,他们比较的肯定不一样,因为首元素地址不同,所以比较就不相同,这里比较的不是内容,是地址。
比较二个字符串应该用strcmp函数
//求字符串比较函数 int my_strcmp(char* arr1, char* arr2) { while (*arr1 == *arr2) { if (*arr1 == '\0') { return 0;//相等 arr1++; arr2++; } return (*arr1 - *arr2); } } int main() { char arr1[] = "abc"; char arr2[]="bcd"; int ret = my_strcmp(arr1, arr2); if (ret > 0) { printf(">0\n"); } else if (ret<0) { printf("<0\n"); } else { printf("==0\n"); } return 0; }
这个代码关键就是*arr1和*arr2相同的情况下遇到'\0时应该终止循环,返回相等。
1.strncpy -字符串拷贝。
2.strncat(字符串连接)
加了n就是加了限定的条件,比如strncmp
3.strstr -----查找子串
如果找到了子串就返回第一个字符的地址,也就是b字符的地址,因为是从b字符开始才算找到子串。找不到返回空指针,NULL
//模拟实现一下strstr
char* my_strstr(char* arr1, char* arr2)
{
char* s1 = arr1;
char* s2 = arr2;
char* p = arr1;
while (*p)
{
s1 = p;
s2 = arr2;
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 =='\0')
{
return p;
}
p++;
}
return NULL;
}
int main()
{
char arr1[] = "www.h";
char arr2[] = "w.h";
char* ret = my_strstr(arr1, arr2);
if (ret == NULL)
{
printf("子串不存在\n");
}
else
{
printf("%s\n", ret);
}
return 0;
}
strtok函数
//strcok()函数
#include <string.h>
int main()
{
char arr1[] = "zhangsan@1666.com";
char arr2[30] = { 0 };
strcpy(arr2, arr1);
const char* p = "@.";
char* s2 = NULL;
// 初始化只是初始话一次
for (s2= strtok(arr2, p); s2 != NULL; s2=strtok(NULL, p))
{
printf("%s\n", s2);
}
return 0;
}
3. strerror?函数的使⽤?
strerror函数可以把参数部分错误码对应的错误信息的字符串地址返回来。?
在不同的系统和C语⾔标准库的实现中都规定了⼀些错误码,⼀般是放在 errno.h 这个头⽂件中说明
的,C语⾔程序启动的时候就会使⽤⼀个全⾯的变量errno来记录程序的当前错误码,只不过程序启动
的时候errno是0,表⽰没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会讲对应
的错误码,存放在errno中,⽽⼀个错误码的数字是整数很难理解是什么意思,所以每⼀个错误码都是
有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。?
#include <errno.h>
int main()
{
for (int i = 0; i < 10; i++)
{
printf("%s\n", strerror(i));
}
return 0;
}
4.memcpy -内存拷贝
模拟实现memcpy函数
void* my_memcpy(void* dest, void* src,size_t num)
{
char* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr1 + 2, arr1, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
//不能解决重叠问题
重叠问题
memmove使⽤和模拟实现
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr1 + 2, arr1, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
//不能解决重叠问题
memset函数的使⽤
//memset内存设置
int main()
{
char arr[] = "hello bite";
memset(arr, 'x', 5);//以字节为单位来设置的
printf("%s\n", arr);
return 0;
}
memcmp函数的使⽤
//memcmp 函数的使⽤
int main()
{
int arr1[] = { 1,2,3,4,5,6 };
int arr2[] = { 1,2,3,4,6 };
int ret = memcmp(arr1, arr2, 17);
printf("%d\n", ret);
return 0;
}
结构体
//结构体 struct stu { int age; char arr[20]; }; struct bb { struct stu s; int v; }; int main() { struct bb s1 = { {12,"cs"},66 }; printf("%d %s %d", s1.s.age, s1.s.arr, s1.v); return 0; }
结构体也可以定义结构体。
结构体的自引用
结构体也可以调用自己,只不过要存自己的的地址------>结构体的自引用
结构体内存对齐
首先找对c1,i,c2对应在内存空间的存储位置,比如c1在偏移量为0的位置,i占4个字节,4与8找一个最小值,就是4了,应该在这个数的整数倍位置存储,图中就是在4这个地方存储,4-7占4个字节,c2就是偏移量8的地方存储了。,0-8找一个最大对齐数的整数倍,最大是是占4个字节的i,0-8有9个字节,找一个4的倍数,那就是12了,12就是4的倍数。
offsetof宏,返回偏移量
#pragma可以修改对齐数。
结构体传参
//结构体传参 struct s { int a[20]; int age; }; struct s f = { {1,2,4},21 }; void print1(struct s f) { int i = 0; for (i = 0; i < 3; i++) { printf("%d ", f.a[i]); } printf("%d\n", f.age); } void print2(struct s*ps) { int i = 0; for (i = 0; i < 3; i++) { printf("%d ", ps->a[i]); } printf("%d\n", ps->age); } int main() { print1(f); print2(&f); return 0; }
结构体传参应该传地址,地址节约内存。
位段
所谓位段就是:后面用比特位来节省空间
如图 位段里的成员是从右到左占字节,如果内存不够则重新开辟新的内存空间
说明假设在vs里成立
使用位段的话节省了一个字节,不然的话就4个字节
1.从右到左使用
2. 如果剩余空间不够下一个成员使用
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员.
通讯录
枚举
枚举的取值默认是从0开始的,往后自加1
//枚举 enum day { a=1,//0+1 b,//1+1 c,//2+1 d,//3+1 }; int main() { printf("%d\n", d); return 0; }
联合
所谓共用体就是共用一块空间
联合体的大小就是这个联合体变量最大的大小
i的二进制内存布局是 01 00 00 00
在联合体 u中 c和i共用一块空间,c对联合体i赋值1,
i的二进制内存布局是 01 00 00 00 ,根据小端存储模式
对c进行返回相当于返回的是首字节的地址,也就是01的地址
最后返回给ret即可
首先对a进行取地址,为什么对取地址a强制类型转换,是因为判断大小端的时候,只需看首字节是否一致,char*类型只访问一个字节,他就可以完成这个事情。
//大小端问题 int chenk_sys() { union u { char a; int b; }c; c.b = 1; return c.a; } int main() { int ret = chenk_sys(); if (ret == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
//}
// 因为i和结构体变量S共用一块空间
union u
{
int i;//4
struct s
{
char a1;
char a2;
char a3;
char a4;
}S;//4
};
int main()
{
union u uu = { 0 };
uu.i = 0x11223344;
printf("%x %x %x %x ", uu.S.a1, uu.S.a2, uu.S.a3, uu.S.a4);
return 0;
}
联合体大小问题
char的字节是1 int的·字节是4 存在联合体对齐数,找对齐数最大的整数倍,比如int的就是最大对齐整数倍,就要浪费3个字节,因为char数组有5个字节,就必须浪费3个字节
char数组再这里开辟了8个字节,自己用;15个字节,int也和char数组公用4个字节
结构体存在内存对齐,联合也存在内存对齐
//联合体大小问题
union u
{
char a[5]; //1,8=1
int i;//4,8=4,找4的倍数的字节。
}uu;
int main()
{
printf("%d\n", sizeof(uu));
return 0;
}
动态内存管理
p在内存中申请的40个字节空间,这个指针p指向的内存的起始地址
所谓动态内存管理就是随便更改开辟内存空间的大小
//malloc函数
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
printf("\n");
return 0;
}
//动态内存管理 #include <stdlib.h> #include <errors.h> int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i) ); } return 0; }
与malloc相匹配的函数是free
malloc是开辟堆区的函数,free是释放堆区的函数,
不用的时候要释放堆区,然后赋给空指针,否则就成为了野指针。
free函数只是释放的指针的空间,还给的操作系统,但是p还记录着之前的那个起始地址,所有指针p现在很危险,如果不置位空指针,那么就很可能成为野指针
malloc函数只是帮你申请空间,但是没有帮你初始化
你要释放哪个空间,就把那个起始地址传给free函数即可
calloc是开辟好了直接初始化,malloc是开辟了重新初始化。
如果已经通过了malloc或者calloc函数获得了动态空间,想改变其大小,可以用recalloc函数程序分配
情况1
当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2
当是情况2的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。
由于上述的两种情况,realloc函数的使⽤就要注意⼀些
realloc函数里为空指针 等价于malloc
常⻅的动态内存的错误
1. 对NULL指针的解引⽤操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
if (p != NULL)///如果p的值是NULL,就会有问题
{
*p = 20;
}
else
{
perrer("malloc");
return 0;
}
free(p);
p= NULL;
}
2.对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
}
3. 对⾮动态开辟内存使⽤free释放
void test()
{
int* p = (int*)malloc(40);//在堆区申请空间
free(p); //空间释放
}
4. 使⽤free释放⼀块动态开辟内存的⼀部分
指针p指向的起始地址+1的地址,我们知道free释放是从起始地址开始释放的
4.5 对同⼀块动态内存多次释放
正确写法
4.6 动态开辟内存忘记释放(内存泄漏)
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放
vs调试技巧
程序运行就成死循环了,为什么呢?、
是因为先创建了i变量,首先说明一下局部变量是在栈区存储的,malloc,calloc,这些开辟内存的函数是在堆区存储的,static修饰的变量或者全局变是在静态区存储的。
有二点
1. 栈区内存的使⽤习惯是从⾼地址向
低地址使⽤的,所以变量i的地址是
较⼤的。
2. 数组在内存中的存放是:随着下标
的增⻓,地址是由低到⾼变化的。
总结:
先创建i变量,i变量就存储在栈区的高地址处。然后又因为数组在内存是连续存放的,然后又因为vs编译器存储地址在数组越界的地址里面多了二个导致数组越界最后一个地址和i的地址重合,就导致消除了i的值。