专栏首页嵌入式知识C语言从内存到指针

C语言从内存到指针

C语言知识语法这一篇就够了(总结)

“合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。”

1、从源程序到可执行程序

C语言从.c文件到可执行文件要经历预处理->编译->汇编->链接个步骤。一般将这四步统称为编译。下面分别看看这几步都干了什么。假设我们有一个a.c文件:

预处理:展开头文件,宏替换、去掉注释、完成条件编译。生成a.i文件 编译 :进行语法检查,将C语言转成汇编。生成a.s文件 汇编 :将汇编代码转成机器语言代码。生成a.o文件 链接 :将多个文件连接到一起生成可执行文件。生成a.out文件。默认去/lib和/usr/lib寻找链接库 集成开发环境一般都将这四个步骤打包成了一个按钮,我们可以在Linux下使用gcc来分开探寻。

//-o选项为指定输出的文件名
gcc -E a.c  -o a.i  //只进行预处理,不进行编译汇编链接,调用gcc工具链中的cpp工具
gcc -S a.c             //只预处理编译,不汇编链接,调用gcc工具链中的ccl工具
gcc -c a.c             //进行预处理,编译汇编,调用gcc工具链中的as工具
gcc a.c                 //进行预处理器,编译,汇编,链接,自指定用-l选项。调用gcc工具链中的ld工具

2、指针与数组

本应放到下面,但这里重要,故提前。

指针就是地址,在一定程度上可以把数组名就看成一个特殊的指针,指针也就是一个变量而已,指针全名指针变量。

指针

(1)指针数组

指针数组,是个数组,里边放的东西都是指针。char *p[2]={"china","linux"};

仔细来说,从运算符优先级来看,[]的优先级比*高,所以p先和[]结合。所以这就是个数组,再与指针结合,所以叫指针数组。 二重指针可以指向指针数组。

(2)数组指针

数组指针,是个指针,指向数组的指针.

int a[2][5]={0}; 
int (*p)[5]=a;        //指向二维数组

int b[2][3][4] = {0}; //多维数组
int(*q)[3][4] = b;

这里指针是在()中放着,())和[]优先级都是最高的,结合性是从左向右,所以先与*结合,这是个指针,然后与[]结合,就是指针数组。

(3)函数指针

int (*p)(int a,char b);//函数指针p指向返回值类型为int的,两个参数为int和char的函数

使用方法如下,
p(a,b);
或者
(*p)(a,b);

void *类型可以指向任何一个类型的指针

void (*signal(int sig, void (*func) (int))) (int)

signal仍然是一个函数,他返回一个函数指针,这个指针指向的函数没有返回值,只有一个int类型的参数

(4)二维数组

a[5][7];//用指针访问就是*(*(p+5)+7)
int (*p)[7]=a;

字符数组与指向字符串指针的问题

其中char *p="linux" 这种情况字符串Linux只存在于只读数据段中(rodata),所以p所指向的内容不可以被更改,如*(p+1)=a;这是实现不了的,但是可以更改指针p 指向的地址,例如p="mengchao",但是这里的p="mengchao"这段话放在子函数中是不可以的,字符串mengchao是存在于栈中,子函数结束就被释放了,不能够达到改变的目的。可以使用二重指针来实现。

char p[]="linux" 这个是字符数组,相当于初始化数组,可以更改内容,如p[1]='a';* 这里Linux存在于栈上

所以有的字符串操作函数例如char *strcat(char *dst,char const *src);前面的参数dst,需要修改,只能传数组,不能直接传一个字符串,因为字符串不可改变

所以想修改字符串,请将他放在字符数组中。 另外关于这个程序

char c = 'a';
char *p = &c;

char *q = "china";

printf("%p\n",p);     //006DFEE7
printf("%c\n",*p);    //a
cout << *p << endl;   //a

printf("%p\n",q);     //004B9024
printf("%s\n",q);     //china
cout << q << endl;    //china
cout << *q << endl;   //c 

这里想插一句关于strlen和sizeof的区别。比如我们定义char buf[100]={"helloworld"}; strlen(buf);就是里边字符串的长度,而sizeof(buf);就是数组长度100.

一定程度上可以认为一级指针与一维数组名等价,二级指针与指针数组名等价,数组指针与二维数组名等价。而二级指针和二维数组名没有一毛钱关系。

数组名

数组名可以看成是首元素的首地址,也可以当成一个整体来看。数组名是常量,才可以唯一的确定数组元素的起始地址。设有一维数组int a[5]:

a[1] = *(a+1); //a代表首元素首地址,加1跨度为int大小为4

对一维数组名和二维数组名引用(取地址)对一维数组名进行引用会将使其升级为二维数组名。

(&a+1)//这个东西加1,加的大小是整个数组的大小,是20.

二维数组名解引用,降维为一维数组名。二维数组名是首元素首地址,设有int a[4][3],则

*(a+1) //代表的是二维数组的第二维
a[1][2] = *(*(a+1)+2)

注意下面的问题

*p++:等同于:*p;  p += 1;先运算再++
解析:由于*和++的运算优先级一样,且是右结合。故*p++相当于*(p++),p先与++结合,然后p++整体再与*结合。前面陈述是一种最常见的错误,很多初学者也是这么理解的。但是,因为++后置的时候,本身含义就是先运算后增加1(运算指的是p++作为一个整体与前面的*进行运算;增加1指的是p+1),所以实际上*p++符号整体对外表现的值是*p的值,运算完成后p再加1.
【注意】是运算后p再加1,而不是p所指向的变量*p再加1
*++p:等同于 p += 1;    *p;先++再运算
解析:由于++在p的前面,++前置的含义是,先加1,得到一个新的p(它的值是原来p的值加1)。然后这个新的p再与前面的*结合.

【总结】无论是*p++还是*++p,都是指针p += 1,即p的值+1,而不是p所指向的变量*p的值+1。
++前置与++后置,只是决定了到底是先p += 1,还是先*p。++前置表示先p += 1,再*p。++后置表示先*p,在p += 1;
--前置与--后置的的分析方法++前置与++后置的一样。

3、数据类型

变量名只能由字母数字和_下划线组成,而且只能以字母或下划线开头,区分大小写。变量命名一般采用驼峰命名法,首字母小写,后面单词的首字母大写比如myFirstName。

浮点数

至于各种数据类型范围大小,不再赘述。要重点注意一下重点在于浮点数的精度问题。浮点数由三个基本成分构成:符号、阶码和尾数。并不能像整型一样精确表示。比如0.3,浮点数可能存储的是0.299999.正如下面的代码,打印的并不是3,而是2.

#include <stdio.h>
int main()
{
    double tmp=0.3;
    printf("%d",tmp*10);
    return 0;
}

字符类型

字符类型要用英文单引号''括起来。char c = 'a';没用引号括起来的,比如char c = 65会将65转成ASCII码对应的字符A。

结构体

《道德经》 曰:"一生二,二生三,三生万物"。结构体用关键字 struct 定义,表达出多个不同变量在一起的类型。我们由此可以使用struct自定义几乎所有想要的类型。

结构体的定义:

struct student
{
    char name[30];
    char sex;
    int age;
    float high;
}stu1;//这里定义了一个变量stu1
struct student stu2;//这里定义了一个变量stu2

这种定义出来的结构体类型在定义变量时必须跟着struct,就像struct student stu1;一样,我们引入typedef,避免这种情况。

typedef struct student
{
    char name[30];
    char sex;
    int age;
    float high;
}STUDENT;//将struct student类型重命名为STUDENT
STUDENT stu, stu2;//使用STUDENT替换struct student定义变量

结构体变量初始化及成员访问:

定义的结构体变量用点成员运算符(.)访问成员变量,比如stu.sex就这样访问成员变量。如果是结构体类型的指针变量则使用->访问成员变量。STUDENT stu; stu->sex = 'x';初始化大概有三种方法。

结构体大小涉及内存对齐。可百度结构体内存对齐

struct Date
{
    char a;
    int b;
    int64_t c;
    char d;
};
Date data [2][10];

结构体所占的内存大小 a.整体所占的内存大小应该是结构中成员类型最大的整数倍,此处最大的类型是int_64t,占8个字节。即最后所占字节的总数应该是8的倍数,不足的补足 b.数据对齐原则-内存按结构体成员的先后顺序排列,当排到该成员变量时,其前面所有成员已经占用的空间大小必须是该成员类型大小的整数倍,如果不够,则前面的成员占用的空间要补齐,使之成为当前成员类型的整数倍。假设是地址是从0开始,结构体中第一个成员类型char型占一个字节,则内存地址0-1,第二成员从2开始,int型所占内存4个字节根据原则b,第一个成员所占内存补齐4的倍数,故第一个成员所占内存:1+3=4;第二个成员占5-8.第三个成员占8个字节,满足原则b,不需要补齐,占9-16第四个成员占一个字节,占17.故总内存为1+3+4+8+1=17个字节,但根据原则1总字节数需是8的倍数,需将17补齐到24.故此结构体总字节数为:24字节

共用(Union)与大小端

共用体占用空间的大小取决于类型长度最大的,共用体变量的地址和它的各成员的地址都是同一地址。同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一种,而不是同时存放几种。共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。

所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;所谓的小端模式,是指数据的低位保存在内存的低地址中,而数 据的高位保存在内存的高地址中。

4、位操作与逻辑操作

位与(&),位或(|),位取反(~),位异或(^):对应相同时 0 ,不同时则为 1。左移(<<),右移(>>).

优先级:() > 成员运算 > (^/!) >算术> 关系 > (>> <<)位逻辑(&|^) > 逻辑 > 赋值 >

逻辑操作:&&,||,!

5、预处理

宏一般都是大写的来表示,还分为有参宏和无参宏,为了防止代入错误,不要吝啬你的括号就行了。

条件编译

#ifdef #endif两个搭配使用

多文件编程

防止头文件重复包含,一般使用

#ifndef__XX_H__
#define__XX_H__
//数据类型声明
//函数声明
#endif

6、关键字作用

static作用

1、修饰变量

静态全局变量:全局变量前加static修饰,该变量就成为了静态全局变量。我们知道,全部变量在整个工程都可以被访问(一个文件中定义,其它文件使用的时候添加extern关键字声明 ),而在添加了static关键字之后,这个变量就只能在本文件内被访问了。因此,在这里,static的作用就是限定作用域。

静态局部变量:局部变量添加了static修饰之后,该变量就成为了静态局部变量。我们知道局部变量在离开了被定义的函数后,就会被销毁,而当使用static修饰之后,它的作用域就一直到整个程序结束。因此,在这里static的作用就是限定生命周期。

2、修饰函数

修饰函数则该函数成为静态函数,函数的作用域仅限于本文件,而不能被其它文件调用

extern作用

1、函数的声明extern关键词是可有可无的,因为函数本身不加修饰的话就是extern。但是引用的时候一样需要声明的。

2、全局变量在外部使用声明时,extern关键字是必须的,如果变量没有extern修饰且没有显式的初始化,同样成为变量的定义,因此此时必须加extern,而编译器在此标记存储空间在执行时加载内并初始化为0。而局部变量的声明不能有extern的修饰,且局部变量在运行时才在堆栈部分分配内存。

3、全局变量或函数本质上讲没有区别,函数名是指向函数二进制块开头处的指针。而全局变量是在函数外部声明的变量。函数名也在函数外,因此函数也是全局的。

4、谨记:声明可以多次,定义只能一次。

5、

extern int i; //声明,不是定义
int i;        //声明,也是定义 

register作用 请求编译器将变量直接放在寄存器中,这样速度快。只作用于局部变量

volatile作用

volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。

  精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。

const作用

const是常变量,修饰的值不可改变,但是实际上,在C语言中这是假的,const修饰的变量是可以修改的,可以通过指针修改。

const int a = 4;
int *pi =&a;
*pi=4;
printf("%d",a);

下面说说指针类型的const

int const *pi;//这是一个指向整形常量的指针,可以修改指针的值,但是不能修改它所指向的值.
int *const pi;//这个是一个指向整形的常量指针,可以修改它所指向的值,但是不能修改指针的值。
int const *const pi;//这就是上边两种的综合了,都不可以修改.

7、进程空间

全局变量:不初始化的默认为0,放在.bss段,初始化为0的也放在.bss段
初始化为非0的全局变量放在.data段中,生命周期全局
局部变量放在栈上,当调用结束,生命周期结束

8、文件操作

文件的打开操作 fopen 打开一个文件 文件的关闭操作 fclose 关闭一个文件 文件的读写操作 fgetc 从文件中读取一个字符          fputc 写一个字符到文件中去          fgets 从文件中读取一个字符串          fputs 写一个字符串到文件中去          fprintf 往文件中写格式化数据          fscanf 格式化读取文件中数据          fread 以二进制形式读取文件中的数据           fwrite 以二进制形式写数据到文件中去          getw 以二进制形式读取一个整数          putw 以二进制形式存贮一个整数 文件状态检查函数 feof 文件结束           ferror 文件读/写出错           clearerr 清除文件错误标志           ftell 了解文件指针的当前位置 文件定位函数 rewind 反绕         fseek 随机定位

版权属于:孟超 本文链接:https://mengchao.xyz/index.php/archives/163/ 转载时须注明出处及本声明

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • int *ptr2=(int *)((int)a+1);

    首先明确,a是一个具有4个整型变量的数组的名字,具体地说是这种数组的首元素的首地址,而&a是数组的首地址,请注意措辞。而关于指针加1,则需要指针运算的知识。没学...

    用户5426759
  • LeetCode做题

    在数组和字符串中经常使用的一种方法就是双指针,使用双指针技巧的典型场景之一是你想要从两端向中间迭代数组。这时你可以使用双指针技巧:一个指针从始端开始,而另一个指...

    用户5426759
  • uboot看这一篇应该就够了!

    U-Boot,全称 Universal Boot Loader,是遵循GPL条款是一个开源项目,用于启动操作系统内核,操作系统并不是一开机就会自动启动,是要有引...

    用户5426759
  • c语言基础学习07_关于指针的复习

    =============================================================================

    黑泽君
  • 程序员C语言快速上手——进阶篇(六)

    由上例可验证,数组的内存空间是连在一起的,它的第一个元素地址是0x22fe30,第二个元素的地址是0x22fe34,紧随其后。因为是int数组,每个元素都需要占...

    arcticfox
  • 近期关于代码审计的学习总结

    这一小段时间对一些 CMS 进行代码审计,和一些 CVE 分析复现。总结一下几个案例的问题产生原因和利用思路。由于能力有限,挖掘到的都并非高危漏洞,旨在总结一下...

    信安之路
  • 字符串的全排列和组合算法

    http://blog.csdn.net/hackbuteer1/article/details/7462447

    bear_fish
  • 匹配追踪算法进行图像重建

    匹配追踪的过程已经在匹配追踪算法(MP)简介中进行了简单介绍,下面是使用Python进行图像重建的实践。

    卡尔曼和玻尔兹曼谁曼
  • LeetCode 958. 二叉树的完全性检验(层序遍历)

    若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。(注:第 h...

    Michael阿明
  • 算法数据结构中有哪些奇技淫巧?

    版权声明:本文为苦逼的码农原创。未经同意禁止任何形式转载,特别是那些复制粘贴到别的平台的,否则,必定追究。欢迎大家多多转发,谢谢。

    帅地

扫码关注云+社区

领取腾讯云代金券