C语言:指针

1. 指针:保存地址的变量 *p (pointer) ,这种变量的值是内存的地址。

  取地址符& 只用于获取变量(有地址的东西)的地址;scanf函数-取地址符
  地址的大小不一定和int型相同,取决于编译器或系统架构是x86还是x64的,有时需要强转为 long long int; 因此将地址交给整型变量是不靠谱的,我们使用指针类型。 验证方法:  %#x - (int)&i   %p %#p - &i   %lu - sizeof

 int i = 0;int p;int a[10];p = (int)&i; //强制类型转换printf("&i = %#x ", &i); //类型不匹配,自动转换printf("p = %#x\n", p);printf("real &i = %p\n", &i);printf("real &p = %p\n", &p);//void *    int               x86 x64printf("%lu\n", sizeof(&i)); // 4/8printf("%lu\n", sizeof(int)); // 4/4printf("%p\n", &a);printf("%p\n", a);printf("%p\n", &a[0]);printf("%p\n", &a[1]);

  (补充:scanf函数传入地址时忘加&编译不报错的原因——32位编译器int和指针大小恰好一样,编译器以为传入的int是地址。)

  了解一下——
(1) C语言相邻定义的变量在内存中连续排列(堆栈stack 自顶向下 高地址 - >低地址);
(2) 数组的地址:数组名就是数组首元素地址 arr &arr &arr[0] ;
(3) 一个内存空间大小1B(16进制保存),4GB内存的内存地址编号有 232 个(所以32位机器的内存封顶就是4G,只能编号这么多个);32位系统的一个内存地址编号大小是4B(32位),64位系统的内存地址编号是8B大小(64位)。

2. 指针变量的定义

定义指向: int *p = &i ; 指针p指向一个int,可以将int变量 i 的地址赋给p (不存在 int * 这种类型 – 其实C编译忽略空格)
       int * p, q; 等价于 int *p, q; 均表示定义指针p和整型q (可以做 p = &q;)
       int *p, *q; 这才定义了两个指针变量。

提醒:千万不要定义了指针后还没有指向变量就使用它(即不能用*p)。

3. 指针的运算

char * p = &i[0]
int * q = &j[0]
p+1和q+1分别表示在地址上移动一个sizeof(type) ,在地址层面p加了1(地址升高1),q加了4,就是让指针指向下一个变量。
所以指针如果不是指向一片连续分配的空间(如:数组; i[1] == * (p + 1)),这样的运算毫无意义。
运算 // + - += -= ++ - -
对于 * p ++ :取出p所指的数据,然后把p移向下一个位置(用于数组类的连续空间操作)。【符号优先级 ++ 高于 * 】
  eg:
    for(p = ai; * p != -1; ) {
      printf(“%d\n”, * p++);
    }

两个同类型指针相减(否则非法)的结果是这两个地址间存在几个该类型变量,而不是地址之差。任意的两个指针(不在同一个数组内的)相减和void * 这样纯粹的地址指向指针一样,不能进行减法运算。
不同类型指针不要相互赋值,用int指针给char数组操作会一次性改变4个char变量(因此也不建议强制类型转换,除非你知道自己在干嘛)
void * 俗称万能指针,纯粹访问某个地址,通常在底层编程中使用(控制寄存器或外部设备等)。【 int * p = &i; void * q = (void * ) p; 这没有改变p的变量类型,而是转变不同的眼光看待p所指的变量】

比较 // < <= == >= > !=
比较地址在内存中的大小,数组中的单元的地址一定是线性递增的。

4. 指针变量的作用

运用指针变量,我们在编程时可以通过地址访问变量。低地址不能访问,如存放ascii码的0~255等
  取内容符 * (解引用)用来访问指针所指向的内存地址处的变量,可以做右值也可以做左值。注意与指针定义时的 * 区别,另外它也不是乘法运算。它与 & 互为反作用( * & q 与 & * p 【p是指针,q是普通变量,不能倒过来写,具体参考运算符优先级C语言学习笔记05】 )。
【左值:出现在赋值号左边的不是变量,而是表达式计算的值,是特殊的值(可以接收另一个值),因此称为左值;a[0] = 2; *p = 31; 。】

0地址:机器实际只有1个0地址,操作系统的进程概念会创建虚拟空间,所以每个程序都有自己的0地址。一般这是不能被访问的地址,可以使返回的指针是无效的(初始化指针为0),它被预定义为NULL(C语言中预定义NULL 0),有的编译器使用NULL和0不一定一样。【建议:一般尽量使用NULL】

 int *p, q;printf("%p %p\n", p, &q);printf("q = %d\n", q); //此时p的地址若是个低地址就禁止访问,若进行访问程序可能崩溃 p = &q;q = 10;printf("%p %p\n", p, &q);printf("*p=%d q=%d\n", *p, q);printf("%d %p\n", *&q, &*p);

当指针作为函数参数时,调用函数时的参数赋值必须用实际变量的地址,在函数内用指针就能访问外部实际的变量。

void f(int *p) {printf("p = %p\n", p);printf("*p = %d\n", *p);*p = 31; //左值printf("*p' = %d\n", *p);
}
void g(int k) {printf("k = %d\n", k);printf("&k = %p\n", &k);k = 1;printf("k' = %d\n", k);
}int main(int argc, char *argv[])
{   int i = 29;printf("&i = %p\n", &i);printf("i = %d\n", i);putchar(10);g(i);printf("i = %d\n", i);f(&i);g(i);return 0;
}

5. 指针应用场景

(1) 交换两个变量的值。

void swap(int *a, int *b) {int t = *a;*a = *b;*b = t;
}swap(&x, &y);

(2) 函数需要返回多个值,某些值就需要指针带回。
  - 传入函数的指针参数实际上用于保存需要带回的结果

(3) 函数返回运算状态,结果需要指针返回。
  - 常用套路是让函数返回特殊的不属于有效范围内的值来表示出错:如常用-1或0(文件操作中)表示计算状态,但当任何数值都是有效的可能结果时需要分开返回状态和计算结果。
  - 在C++和java中采用异常机制来解决这个问题。

6. 指针与数组、const

  C语言学习笔记09-数组中说到:传入函数的数组不能在函数内计算出数组长度——因为传入的是数组首元素的地址,即函数内使用的数组和传入前外面的那个数组是同一个,说明传入函数的数组参数本质是个指针。因此,传入函数的数组参数可以写成 int a[] 也可以写成 int *a ,int * ,int [],它们作为函数原型参数都是等价的。
  数组变量是特殊的指针(const的指针)!数组名和数组首元素地址一样 a == &a[0] —— int a[10]; int *p = a; //不用& 。但是数组的单元表达的是变量,需要用&取地址。因此,指针也能看成是长度为1的数组 p[0] == * p 。 int * const a -> int a[],常量指针一旦得到了某个变量的地址就不能再指向其它变量,所以两个数组间不能直接相互赋值。【具体说明:若 int * const q = &i; 此时q(一个地址)不允许被改变,q++ 是错误的,但是,*q = 26; 是可以的。】
  const修饰指针( const 在 * 之前还是之后 )的差异:const int * p = &i 或 int const * p = &i 表示不能用指针去修改变量 i ,但 i 可以直接修改(i = 10),指针 p 也能重新指向其它的变量(p = &j);只有 * p = 10不被允许。【使用场景:传入函数的结构体参数使用const指针,既能用较少的字节数传递值又能避免函数对外面变量的修改。
  const修饰数组,表示数组的每个单元都是const int型,此时必须通过集成初始化进行赋值。同样为了保护数组传入函数后不被破坏,就可以在函数定义时这样写:int sum(const int a [], int len);

7. 动态内存分配

C99支持用读入变量来定义数组(虽然数组创建后也不能改变大小了),但在ANSI C时期程序员采用动态内存分配的方式解决类似的问题。如下: (malloc函数 头文件stdlib.h)
int * a = (int * )malloc(n * sizeof(int) ); //可申请 4 * n 个字节 malloc返回void * ,所以需要强制转换

  malloc函数与calloc函数比较 ——
  void *malloc(size_t num); malloc参数只有无符号整型的num表示分配的字节数。
  void *calloc(size_t num, size_t size); 两个参数分别是无符号整型num表示分配的对象的个数,以及无符号整型size表示每个对象的大小。【比malloc更好的是calloc会初始化分配的空间,而malloc不行 , 使用如: calloc(n, sizeof(type)) 】

这时,a已经可以被当作数组使用。如:&a[i]。使用结束后记得free(a),动态分配借的要还回去。【free还的只能是借的首地址(谁开始找系统借的),如果不是程序会报错】

7.1. 查看本机剩余最大可用内存空间:(如果没有空间了,malloc申请失败返回0,也可以说是NULL)


如果上面while中的判断条件未加(),会提示以下错误:
[Warning] suggest parentheses around assignment used as truth value [-Wparentheses]
这是因为编译器生怕你在编程时搞混了赋值和比较,因此希望你明确自己要做的操作,改成图片中的样子就ok了。

7.2. 接收不定长字符串:(malloc、realloc、free)
 int cnt = 0, len = 20;char ch;char *s1 = (char*)malloc(len);char *s2 = NULL; //出于安全性优化考虑,添加*s2if (s1 == NULL) {return; //直接返回被调用点}while ((ch = getchar()) != EOF) { //windows ctrl+Z    linux ctrl+D  关闭程序ctrl+Cs1[cnt++] = ch; //s1[cnt] = ch;cnt++;可合并成s1[cnt++] = ch;if ( cnt >= len ) {len += 20; // *= 2s2 = (char*)realloc(s1, len);if ( s2 == NULL ) {free(s1);return;}else s1 = s2;}}s1[cnt] = '\0'; //如果不加本行,输出末尾会多出符号直至遍历到存放 0 / '\0'的内存单元puts(s1);free(s1); //切记释放内存free(s2);

动态申请内存空间后,记住合适的时机释放它,虽然一般初学编程时程序小,运行结束后操作系统会帮你善后处理,但是不释放这个习惯如果保留到较大的程序编程中就会有问题。

7.3. 实现可变数组【自定义数组结构】:(memcpy string):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>const int BLOCK_SIZE = 20; //每20个单元一个区//定义自己的数组结构,实现可变数组
//可变数组缺陷:每次需要申请更大的线性连续空间,不够高效
typedef struct {int *array;int size;
} Array; //此处如果改成指针类型*Array 弊端在于想要创建一个本地Array变量就做不出来了,且易造成误解Array array_create(int init_size); //创建数组
void array_free(Array *a); //释放数组空间中的*array
int array_size(const Array *a); //计算数组可用单元数
int* array_at(Array *a, int index); //用于访问数组某个单元(左值、右值)
int array_get(const Array *a, int index);
void array_set(Array *a, int index, int value);
void array_inflate(Array *a, int more_size); //可变数组核心函数:使数组变大/*
* ****** Main ******
*/
int main(int argc, const char *argv[])
{int size = 20; //100Array a = array_create(size);//  printf("array a's size: %d\n", array_size(&a));
//  *array_at(&a, 0) = 123; //array_set(&a, 0, 321);
//  printf("%d\n", *array_at(&a, 0)); //printf("%d\n", array_get(&a, 0));int number = 0;int cnt = 0;//输入数等于-1时表示退出while ( number != -1 ) {//scanf("%d", array_at(&a, cnt++));scanf("%d", &number);if ( number != -1 ) {*array_at(&a, cnt++) = number;}}printf("array a's size: %d\n", array_size(&a));for ( cnt -= 1; cnt >= 0; cnt-- ) {printf("%d\t", *array_at(&a, cnt));}array_free(&a);return 0;
}//typedef struct {//  int *array;
//  int size;
//} Array;Array array_create(int init_size) //Array* array_create(Array *a, int init_size) a->size
{Array a;a.size = init_size;a.array = (int*)malloc(sizeof(int) * a.size);//返回本地Array类型变量可以让外部的调用者更灵活地做计算,若用指针会很麻烦//如需要判断指针是否null,是否之前创建过了(需要先free)return a;
}void array_free(Array *a)
{free(a->array);//再加两重保险,防止外部free后再次调用a->array = NULL;a->size = 0;
}//封装,隐藏内部细节,安全性高
int array_size(const Array *a)
{return a->size;
}int* array_at(Array *a, int index)
{if ( index >= a->size ) { //index < 0//array_inflate(a, index - a->size + 1); //不经济array_inflate(a, (index/BLOCK_SIZE + 1) * BLOCK_SIZE - a->size); //不一定只增加1个block}//返回指针而不是值,可以让外部调用者将其加*当做左值使用return &(a->array[index]); //优先级搞不清了,工程中通常直接加括号
}
//另一种方式:避免 *函数
int array_get(const Array *a, int index)
{return a->array[index];
}
void array_set(Array *a, int index, int value) //修改
{a->array[index] = value;
}void array_inflate(Array *a, int more_size) //理论上数组不能自增长,如何实现可变数组
{//申请一块新的空间int *p = (int*)malloc(sizeof(int) * (a->size + more_size));
//  int i;
//  for ( i=0; i < a->size; i++ ) {//      p[i] = a->array[i];
//  } //memcpy(dst, src, nbytes) //string.hmemcpy(p, a->array, sizeof(int) * a->size);free(a->array);a->array = p;a->size += more_size;
}

8. 返回指针的函数 —— (返回传入的指针)

返回本地变量的地址是危险的。因为离开函数后本地变量不存在了,这个本地变量的地址可能会被重新分配给其它变量使用:


可以看到,p指向的地址变化了,有时甚至程序会崩溃!返回指针的函数,使用全局变量或静态变量相对安全(线程不安全,函数不可重用)。【注意不要使用全局变量在函数间来回传递参数和结果】
最好的做法是返回传入的指针:在主函数里建一个指针给其它函数,其他函数回传指针。

另外看一种特殊的指针 —— 函数指针

函数名也代表一个地址,指针用来存放地址,函数类型的指针可以有:

函数指针能做什么?请看:

更主要是用于
(1)接受用户输入后选择该干什么事(调用哪一个函数),可以比switch更简洁。

#include <stdio.h>void f(int i) {printf("in f(), %d\n", i);
}void g(int i) {printf("in g(), %d\n", i);
}void h(int i) {printf("in h(), %d\n", i);
}void k(int i) {printf("in k(), %d\n", i);
}/* *** */
int main()
{int i = 0;scanf("%d", &i);void (*fa[])(int) = {f,g,h,k}; //函数指针数组集合初始化if ( i >= 0 && i < sizeof(fa)/sizeof(fa[0]) ) {(*fa[i])(0);}//    switch ( i ) {//      case 0: f(0);break;
//      case 1: g(0);break;
//      case 2: h(0);break;
//  }// if ( i == 0 ) {//      f(0);
//  }
//  else if ( i == 1 ) {//      g(0);
//  }}

(2)向函数中传入函数:

#include <stdio.h>int plus(int a, int b) {return a + b;
}
int minus(int a, int b) {return a - b;
}
void cal(int (*f)(int, int)) {printf("%d\n", (*f)(2,3) );
}/* *** */
int main()
{cal(plus);cal(minus);}

C语言学习笔记10-指针(动态内存分配malloc/calloc、realloc、释放free,可变数组实现;Tips:返回指针的函数使用本地变量有风险!;最后:函数指针)相关推荐

  1. C++ Primer Plus学习笔记之类和动态内存分配

    前言 个人觉得学习编程最有效的方法是阅读专业的书籍,通过阅读专业书籍可以构建更加系统化的知识体系. 一直以来都很想深入学习一下C++,将其作为自己的主力开发语言.现在为了完成自己这一直以来的心愿,准备 ...

  2. c语言遍历文件内容_C语言学习第28篇---动态内存分配剖析

    为什么C语言要动态分配内存的意义? 1.C语言中的一切操作都是基于内存的 2.变量和数组都是内存的别名 ---内存分配由编译器在编译期间决定的 ---定义数组的时候必须指定数组长度 ---数组长度是在 ...

  3. c语言malloc引用类型作参数,c语言中动态内存分配malloc只在堆中分配一片内存.doc...

    c语言中动态内存分配malloc只在堆中分配一片内存 .C语言中动态内存分配(malloc)只在堆中分配一片内存,返回一个void指针(分配失败则返回0),并没有创建一个对象.使用时需要强制转换成恰当 ...

  4. 关于动态内存分配malloc的初级用法和注意事项

    #include <stdio.h> #include <stdlib.h> int main(int argc, const char *argv[]) { /*your c ...

  5. 读书笔记||类和动态内存分配

    一.动态内存和类 C++在分配内存的时候是让程序是在运行时决定内存分配,而不是在编译时再决定.C++使用new和delete运算符来动态控制内存.但是在类中使用这些运算符将导致许多新的编程问题,在这种 ...

  6. C语言第十三课,动态内存分配

    动态内存分配的空间放在堆区.动态内存函数主要有:malloc,calloc,realloc 动态内存函数的介绍 malloc 申请一个空间,大小是size的大小,指向的一个类型不明,因为在设计的时候, ...

  7. linux c free大段内存,Linux C 动态内存分配--malloc,new,free及相关内容

    一.malloc()和free()的基本概念以及基本用法: 1.函数原型及说明: void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针 ...

  8. 内存分布malloc/calloc/realloc/free/new/delete、内存泄露、String模板、浅拷贝与深拷贝以及模拟string类的实现

    内存分布 一.C语言中的动态内存管理方式:malloc/calloc/realloc和free 1.malloc: 从堆上获得指定字节的内存空间,函数声明:void *malloc (int n); ...

  9. C++学习笔记-DLL中动态内存管理

    动态内存管理 在dll中malloc的内存,必须在dll中free 注:这是由Windows自己的特点决定! 如果 a 编译成静态库,有下面两种解决方法: 1.b.dll 和 c.dll 使用同一个款 ...

最新文章

  1. Linux/ubuntu server 18.04 安装远程桌面--vnc server
  2. gui窗口遮挡算法_基于 C 语言开发的 GUI 框架
  3. 七年程序员生涯,我学到的重要六课
  4. 机器人运维时代已来临?这是真的......
  5. HDU1421 搬寝室
  6. jmeter从mysql取值_Jmeter获取数据库值并作为参数请求(转载)
  7. idea 新建springboot 的 web 项目
  8. 鸿蒙手机播放音乐-第一集
  9. Asp.Net Core发布绑定域名和端口
  10. java相关的国际化步骤_Java语言资源国际化步骤
  11. vscode之parcel清空dist目录
  12. 软硬负载之间的对比及优缺点
  13. Ubuntu 19 ✖64安装GDAL
  14. C语言条件运算符(?:)的使用
  15. autojs声明文件
  16. 程序集定义(Assembly Definition File)
  17. SQL Server电影院数据库管理系统【英文版-源码】--(Movie Theatre Management System Database)
  18. git学习——上传项目代码到github
  19. 零基础快速搭建rxjava框架
  20. Internet Download Manager(IDM)网页下载浮动条不出现的问题记录

热门文章

  1. go运行报错:command-line-arguments
  2. 数据分析思维 -- 第二步:开启分析思路
  3. 零难度安装法:从VHD启动Win10(更新)
  4. arduino/Mixly使用LGT8F328P
  5. 华为手机免root改mac_华为手机这些默认设置一定要改,不然会卡顿占内存加耗电...
  6. 【转】Linux那些事儿 之 戏说USB(23)设备的生命线(二)
  7. 使用openCV+Qt+fcgi 为OpenStreetMap瓦片添加热力图图层
  8. 图片免费转pdf图片、图片免费转成word、图片免费转excel表格
  9. mysql 修改年龄_MYSQL——数据修改
  10. NetSuite SuiteAnalytics 高级搜索 全课程中文学习视频