c语言提高学习笔记——02-c提高02day

在学习c语言提高总结了笔记,并分享出来。有问题请及时联系博主:Alliswell_WP,转载请注明出处。

02-c提高02day

目录:
1、函数调用模型
2、调用惯例
3、函数变量传递分析
4、栈的生长方向和内存存放方向
5、指针
(1)指针变量
(2)野指针和空指针
1)空指针
2)野指针
(3)间接访问操作符
(4)指针的步长
(5)指针的意义_间接赋值
(6)指针做函数参数
(7)字符串指针强化
1)字符串指针做函数参数
2)字符串拷贝
3)字符串反转
 (8)sprintf

1、函数调用模型

注意:宏函数并不是真正的函数!真正的函数有返回值,参数和函数体。

使用宏函数是因为在一定场景下宏函数效率要比函数高!(宏函数只是预处理器进行简单的文本替换。不过是以空间换时间。)对于频繁使用,并且短小的函数,我们一般使用宏函数代替,因为宏函数没有普通函数调用的开销(函数压栈、跳转、返回等)

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 #define MYADD(x,y) ((x)+(y)) //宏函数
 7 
 8 int main(){
 9     int a = 10;
10     int b = 20;
11 
12     printf("a+b=%d
", MYADD(a,b));
13 
14     system("pause");
15     return EXIT_SUCCESS;
16

没有栈就没有函数?

(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。在解释为什么栈如此重要之前,我们先了解一下传统的栈的定义:
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).
在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。
栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面:
■函数的返回地址;
■函数的参数;
■临时变量;
■保存的上下文:包括在函数调用前后需要保持不变的寄存器。

 1 int func(int a,int b){
 2 
 3 int t_a=a;
 4 int t_b=b;
 5 
 6 return t_a+t_b;
 7 }
 8 
 9 int main(){
10 
11 int ret=0;
ret = func(10,20); 12 int a=2013 14 return EXIT_SUCCESS; 15 }

c语言提高学习笔记——02-c提高02day
c语言提高学习笔记——02-c提高02day

注意:1)左侧30为寄存器中的值。

2)返回地址为下一条指令要执行的地址!

c语言提高学习笔记——02-c提高02day

图中问题1和2由2、调用惯例解决!

2、调用惯例

现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。

如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。(1中图中第1个问题)

因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例(Calling Convention)"一个调用惯例一般包含以下几个方面:

函数参数的传递顺序和方式
函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。

栈的维护方式

在函数将参数压入栈中之后函数体会被调用此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。

为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是:
int_cdecl func(int a,int b);

注意:cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在cdecl 这样的关键字,而是使用_attribute((cdecl)).


c语言提高学习笔记——02-c提高02day

c语言提高学习笔记——02-c提高02day

1中图中第2个问题)由cdecl知道出栈方为函数调用方,所以函数参数销毁由main函数完成。

3、函数变量传递分析

c语言提高学习笔记——02-c提高02day

c语言提高学习笔记——02-c提高02day

c语言提高学习笔记——02-c提高02day

4、栈的生长方向和内存存放方向

c语言提高学习笔记——02-c提高02day

 1 //1.栈的生长方向
 2 
 3 void test01(){
 4 
 5 int a=10 6 int b=20 7 int c=30 8 int d=40 9 
10 printf("a=sd
",&a);
11 printf("b=sd
",&b);
12 printf("c=sd
",&c);
13 printf("d=sd
",&d);
14 //a的地址大于b的地址,故而生长方向向下
15 }
16 
17 //2.内存生长方向(小端模式)
18 void test02(){
19 
20 //高位字节->地位字节
21 int num=0xaabbccdd;//4个字节的16进制数
22 unsigned char* p=&num;
23 
24 //从首地址开始的第一个字节
25 printf("%x
",*p);
26 printf("%x
",*(p+1));//p+1的地址比p的地址高
27 printf("%x
",*(p+2));
28 printf("%x
",*(p+3));
29 }

 分析如下:

c语言提高学习笔记——02-c提高02day

5、指针

char *说到底只是一个地址的序号,无论是char *p,还是double *p,其字节数只和计算机的位数相关,和其数据类型默认所占大小无关。32位系统占4个字节,64位系统占8个字节!

指针是一种数据类型
(1)指针变量
指针是一种数据类型,占用内存空间,用来保存内存地址。
c语言提高学习笔记——02-c提高02day

 (2)野指针和空指针

1)空指针
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。

对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未执行任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。

如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。

不允许向NULL和非法地址拷贝内存:

 1 void test(){
 2     char* p=NULL;
 3     //给p指向的内存区域拷贝内容
 4     strcpy(p,"1111");//err
 5 
 6     char* q=0x1122 7     //给q指向的内存区域拷贝内容
 8 
 9     strcpy(q,"2222");//err
10 
11 }

 2)野指针

在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

什么情况下回导致野指针?
指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

指针释放后未置空
有时指针在free或delete后未赋值NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存释放后的指针应立即将指针置为NULL,防止产生“野指针"

指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

1 void test(){
2     int* p = 0x001;//未初始化
3     printf("%p
",p);
4     *p = 100;
5 }

操作野指针是非常危险的操作,应该规避野指针的出现:
初始化时置NULL
指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成NULL指针,它的缺省值是随机的。

释放时置NULL
当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,是使用if语句测试该指针是否为NULL。

(3)间接访问操作符

通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。

注意:对一个int*类型指针解引用会产生一个整型值,类似地,对一个float*指针解引用会产生了一个float类型的值。

在指针声明时,*号表示所声明的变量为指针

在指针使用时,*号表示操作指针所指向的内存空间

1)*相当通过地址(指针变量的值)找到指针指向的内存,再操作内存

2)*放在等号的左边赋值给内存赋值,写内存

3)*放在等号的右边取值从内存中取值,读内存

(4)指针的步长

指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

思考如下问题?

 1 int a = 0xaabbccdd;
 2 unsigned int* pl = &a;
 3 unsigned char* p2 = &a;
 4 
 5 //为什么*加1打印出来正确结果?
 6 printf("%x
",*p1);
 7 //为什么*p2没有打印出来正确结果?
 8 printf("%x
",*p2);
 9 
10 //为什么p1指针+1加了4字节?
11 printf("pl =sd
",pl);
12 printf("pl+1=%d
",pl +1);
13 
14 //为什么p2指针+1加了1字节?
15 printf("p2 =sd
",p2);
16 printf("p2+1=%d
",p2+1);

深入理解

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 
 7 void test01()
 8 {
 9     char buf[1024] = {0};
10     int a = 100;
11     memcpy(buf+1, &a, sizeof(int));
12     
13     char* p = buf;
14     //如何取出100?(p+1)先定位至100的其实地址,然后(int*)(p+1)取出四个字节,最后解引用
15     printf("*(int*)(p+1) = %d
", *(int*)(p+1));
16 }
17 
18 int main(){
19 
20     test01();
21     
22     system("pause");
23     return EXIT_SUCCESS;
24 }

加深理解

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 #include<stddef.h> //计算偏移量
 6 
  using namespace std;

7 struct Person 8 { 9 int a; 10 char b; 11 char buf[64]; 12 int d; 13 }; 14 15 void test() 16 { 17 struct Person p = {10, 'a', "hello world!", 100}; 18 //测试b偏移量 19 printf("a off:%d ", offsetof(struct Person, b)); 20 //&p取到p的首地址,(char*)&p偏移量每次一个字节,offsetof(struct Person, d)计算d相对于结构体的总量,((int*)((char*)&p + offsetof(struct Person, d)))计算得到d存储的偏移地址 21 printf("*((int*)((char*)&p + offsetof(struct Person, d))) off:%d ", *((int*)((char*)&p + offsetof(struct Person, d))));//输出100 22 23 } 24 25 int main(){ 26 27 test(); 28 29 system("pause"); 30 return EXIT_SUCCESS; 31 }


(5)指针的意义_间接赋值
1)间接赋值的三大条件
通过指针间接赋值成立的三大条件
【1】2个变量(一个普通变量一个指针变量、或者一个实参一个形参)
【2】建立关系
【3】通过*操作指针指向的内存

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 void changeValue(int* p)
 7 {
 8     *p = 100;
 9 }
10 
11 void test()
12 {
13     int a = 10;
14     changeValue(&a);
15     printf("%d
", a);
16 }
17 
18 
19 
20 int main(){
21 
22     test();
23     
24     system("pause");
25     return EXIT_SUCCESS;
26 }

 因为QT每次分配的内存空间固定,可直接操作内存空间。

 1 #include <stdio.h>
 2 
 3 int main(int argc,char *argv[])
 4 {
 5     int a = 10 6 
 7     printf("%d
",&a);
 8 
 9     //先执行前两行代码,确定内存地址,如2686652,然后操作内存地址
10 
11     *(int*)2686652 = 100//类似于int* p = 2686652; *p = 100;
12     
13     printf("a = %d
",a);
14 
15     return 016 }

深入理解(二级指针):

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 void changePointer(int** val)
 7 {
 8     *val = 0x008;
 9 }
10 
11 void test()
12 {
13     int* p =NULL;
14     changePointer(&p);
15     
16     printf("p = %x
", p);
17 }
18 
19 
20 
21 int main(){
22 
23     test();
24     
25     system("pause");
26     return EXIT_SUCCESS;
27 }

间接赋值的推论
·用1级指针形参,去间接修改了0级指针(实参)的值。
·用2级指针形参,去间接修改了1级指针(实参)的值
·用3级指针形参,去间接修改了2级指针(实参)的值。
·用n级指针形参,去间接修改了n-1级指针(实参)的值。

(6)指针做函数参数
指针做函数参数,具备输入和输出特性:
■输入:主调函数分配内存
■输出:被调用函数分配内存

1.主调函数分配内存,被调函数使用内存,指针的输入特性
 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 1.主调函数分配内存,被调函数使用内存,指针的输入特性
 7 void printString(const char* val)
 8 {
 9     printf("打印内容:%s
", str);
10 }
11 
12 //打印指针数组
13 void printStringArray(char** arr, int len)
14 {
15     //arr[0]等价于*(arr + 0),是char*类型的
16     for(int i = 0; i < len; ++i)
17     {
18         printf("%s
", arr[i]);
19     }
20 }
21 
22 //打印数组
23 void printIntArray(int* arr, int len)
24 {
25 }
26 
27 void test()
28 {
29     //堆上分配内存,注意这种用法!
30     char* s = malloc(sizeof(char)* 100);
31     memset(s, 0, 100);
32     strcpy(s, "I am Polly!");
33     printString(s);
34     
35     //数组名左函数参数就会退化为指向数组首元素的指针
36     int arr[] = {1, 2, 3, 4, 5};
37     
38     
39     //栈上分配
40     char* strs[] = {"aaaaa","bbbbb", "ccccc", "ddddd", "eeeee"};
41     int len = sizeof(strs)/sizeof(strs[0]);//等价于len = sizeof(strs)/sizeof(char*);
42     printStringArray(strs, len);
43     
44 }
45 
46 int main(){
47 
48     test();
49     
50     system("pause");
51     return EXIT_SUCCESS;
52 }
2.输出特性:被调函数分配内存,主调函数使用内容
 1 #define _CRT_SECURE_NO_WARNINGS
 2 #include<stdio.h>
 3 #include<string.h>
 4 #include<stdlib.h>
 5 
 6 2.输出特性:被调函数分配内存,主调函数使用内容
 7 void allocateSpace(char** temp)
 8 {
 9     char* p = malloc(100);
10     memset(p, 0, 100);
11     strcpy(p, "hello world!");
12     *temp = p;
13 }
14 
15 
16 void test()
17 {
18     char* p = NULL;
19     allocateSpace(&p);
20     printf("p = %s
", p);
21     
22     if(p != NULL)
23     {
24         free(p);
25         p = NULL;
26     }
27 }
28 
29 int main(){
30 
31     test();
32     
33     system("pause");
34     return EXIT_SUCCESS;
35 }

(7)字符串指针强化

1)字符串指针做函数参数

字符串基本操作

 1 //字符串基本操作
 2 //字符串是以0或者’