C语言学习 — 整体细节说明

最近把唐老师的C语音教学视频看了看,有些知识点还是记下来比较好
	..添加目录栏目 										2021/9/17 
	..指针章节添加不同类型指针分析笔记 						2021/10/19  

1、变量内存区域

在现代计算机系统中,物理内存被分为不同区域
区域不同,用途不同,不同种类的变量位于不同的区域

  • 全局数据区: 存放全局变量,静态变量
  • 栈空间: 存放函数参数,局部变量
  • 堆空间:用于动态创建变量

在这里插入图片描述

static修饰的局部变量

int global;
int func(int x)
{
	static int var;
	var += x;
	return var;
}
int main()
{
	int i =0;
	
	for(i=0;i<=5;i++)
		printf("func(%d) = %d\n",fun(i));
	
	printf("func(0)=%d",fun(0));
	printf("global = d%\n",global);
	return 0; 
}

结果

上面的例子中,var 被static 修饰了,所以var 变量 存储于 全局数据区
static int var; 没有初始化数值,
全局数据中的变量,默认初始化为0
函数返回var的值,虽然是一个局部变量,但是var的值依然保存着返回时候var的值
var只做一次初始化

2、函数相关

下面的额函数调用了几个参数

int main()
{
	func(((1,2),(3,45)));
	return 0;
}

答案是 1个参数,注意括号

而且函数的调用,是可以使用表达式(都好表达式,以逗号最后一个参数为准,具体看例子):

int func(int var)
{
	var++return var;
}
int main()
{
	int r = func(((1,2),(3,45)));
	printf("r= %d\n",r);
	return 0;
}

上面的函数执行输出的结果为 6 ,其实就是 5++ ,逗号表达式从左至右计算结果, 结果为6
再来看一个例子

int func(int a, int b)
{
 	return  a+b;
}
int main()
{
	int x = 6,y = 7, z = 8;
	int r = 0;
	r = func((x--, y++, x+y), z--);
	printf("%d\n",r);
	return 0;
}

函数的调用使用了 func((x–, y++, x+y), z–) 这个,是可以的,其中参数 a 是(x–, y++, x+y)表达式的结果,逗号表达式总左至右计算,5,8, 5+8 =13 z先取值8 ,再 --, 所以结果是21

我们知道,实参只是用来初始化形式参数,并不会改变实参的值,相当于在调用的时候赋值给形式参数,所以本身实多少还是多少,与函数中的运算无关

关于数组元素作为函数的参数,是会改变素组元素中的值得,和其他的形参不一样,是不是可以理解为对数组的操作其实就是对指针指向的地址参数的操作,具体分析请看后面的内容,指针部分的解析。

3、逗号表达式

在这里插入图片描述
不良示例:

#include<stdio.h>
void hello()
{
	printf("hello!\n");
}
int main()
{
	int a[3][3] ={   //{2,5,8},前3个元素 2,5,8,后面全部为0
		 (0,1,2),
		 (3,4,5),
		 (6,7,8),
	};
	
	int i = 0;
	int j = 0;
  
  while(i < 5)
  	printf("i = %d\n",i),
  hello(),
  i++;
  for(i=0; i<3; j++)
  {
  	for(j=0; j<3; j++)
  	{
  		printf("a[%d][%d] = %d\n",i,j,a[i][j]);
  	}
  }
  return 0;
}

在这里插入图片描述

一行代码实现strlen

#include<stdio.h>
#include<assert.h>

int strlen(const char* s)
{
    return assert(s),(*s ? strlen(s + 1) + 1 : 0);
}
int main()
{
    printf("len = %d\n",strlen("qianzhiheng"));  
    printf("len = %d\n",strlen(NULL)); 
    return 0;
}

在这里插入图片描述

4、指针: 一种特殊的变量

  • 指针是C语言中的变量 ,指针专用于保存程序元素的内存地址
  • 可使用 * 操作符 通过指针访问程序元素本身
  • 指针也有类型 , 指针类型由 数据类型 + * 构成
  • 指针是变量,因此赋值时必须保证类型相同
  • 指针变量保存的地址必须是有效地址
  • 通过指针参数:
    能够实现函数交换变量的值
    能够从函数中“返回”多个值(return 只能返回一个值!)

数组

  • 数组的本质是一片连续的内存
  • 数组名并不是指针,只是代表了0号元素的地址,因此可以当做指针使用
  • &a 与 a 在数值上相同,但是意义上不同
  • C语言中的字符串常量的类型是 char*
  • 当指针指向数组元素时, 才能进行指针运算
    i = *p++; p要指向数组元素时,这个语句才是正确的

函数

  • 函数的本质是一段内存中的代码(占用一片连续内存)
  • 函数拥有类型,函数类型由 返回类型 参数类型列表 组成:
    在这里插入图片描述
  • 函数名就是函数体代码的起始地址 (函数入口地址)
  • 通过函数名调用函数, 本质为指定具体地址的跳转执行
  • 因此,可定义指针,保存函数入口地址

函数指针( Type func (Type1 a, Type2 b))

  • 函数名即入口地址, 类型为 Type (*) (Type1 ,Type2)
  • 对于func 的函数, &funcfunc 数值相同,意义相同
  • 指向函数的指针: Type (*pFunc)(Type1 ,Type2)=func;
  • 函数指针的本质还是指针(变量,保存内存地址)
  • 可定义函数指针参数,使用相同代码实现不同功能

函数指针的应用请看下面的例子

#include "stdio.h"
#include "string.h"

int add(int a, int b)
{
    return a + b;
}

int mul(int a, int b)
{
    return a * b;
}

int calculate(int a[], int len, int(*cal)(int, int))
{
    int ret = a[0];
    int i = 0;

    for(i=1; i<len; i++)
    {
        ret = cal(ret ,a[i]);//调用了add,    1.ret = add(ret, a[1])  ==>  add(a[0], a[1]) ==> a[0] + a[1]
         							 //   2.ret = add(ret, a[2])  ==> a[0] + a[1] + a[2]
    }
    return ret;
}

int main()
{
	int a[] = {1, 2, 3, 4, 5};
    int (*pFunc)(int, int) = NULL;

    pFunc = add;

    printf("%d\n",pFunc(1, 2));
    printf("%d\n",(*pFunc)(3, 4));

    pFunc = &mul;

    printf("%d\n",pFunc(5, 6));
    printf("%d\n",(*pFunc)(7, 8));

    printf("1 + ... + 5 = %d\n",calculate(a, 5, add));
    printf("1 * ... * 5 = %d\n",calculate(a, 5, mul));
	return 0;
}

在这里插入图片描述

再论数组参数

函数的数组形参退化为指针! 因此,不包含数组实参的长度信息。使用数组名调用时,传递的是 0号元素的地址。

请看下面的例子:

int demo(int arr[], int len) // 实际上函数变成这样了 int demo(int *arr, int len)
{
    int ret = 0;
    int i =0;
    
    // printf("demo: sizeof(arr) = %d\n", sizeof(arr));//gcc编译器这里编译不通过
    printf("arr = %p\n", arr); 
    while(i < len)
    {
        ret += *arr++; //*arr++ 这个算法只有在arr 是一个指针,并且指向数组元素时才正确,如果这里编译通过,说明arr是一个指针        
        i++;
    }
    return ret;
}

int main()
{
	int a[] = {1, 2, 3, 4, 5};

    // int v = *a++; 这句话编译不会通过,因为a是数组名,不是指针

    printf("a = %p\n", a);
    printf("return value: %d\n",demo(a, 5));

	return 0;
}

在这里插入图片描述

void指针类型

  • void 类型是 基础类型 ,对应的指针类型为 void*
  • void*是指针类型,其指针变量能够保存地址
  • 通过void* 指针无法获取内存中的数据 (无长度信息)
char c = 0;
int i = 1;
float f = 2.0f;
double d = 3.0;

void* p = NULL;
double* pd = NULL;
int* pi = NULL;
/*
4个赋值语句都正确
void* 指针可以保存任意类型的地址
*/
p = &c;
p = &i;
p = &f;
p = &d; 

printf("%f\n, *p");// 错误的,无法访问
/*
void* 类型指针无法访问内存中的数据
void* 类型的变量可以合法的赋值给其他具体数据类型的指针变量
其他指针类型的变量不能相互赋值
*/
pd = p; //合法的
pi = P; //合法的
pd = pi ;// 错误的,不合法

堆空间的使用

  • 工具箱:stdlib.h
  • 申请: void* malloc(unsigned bytes)
  • 归还: void free(void* p)
  • malloc 申请内存后,应该判断是否申请成功 if(p != NULL)

注意:自己一直有一个问题,什么时候需要用到malloc申请堆空间的地址呢?
因为项目中基本上都没使用到过malloc,自己查了下网上的观点,总结一下:
1、有时候,需要多大的空间事先并不知道,只有在程序运行时候才知道,所以需要使用malloc 动态分配内存空间,比如需要输入n个人员的资料,这个n不确定有多少的时候,就可以使用动态分配;
2、但是,我可以说,我直接申请一个足够大的数组,大到完全可以存放一般情况下的人员资料情况, 这也可以的,只是这样的程序不是很“合理”,一般来说,栈空间容量是远小于堆空间,你在栈空间申请不能过大,而且过大往往大部分时候都是浪费。
3、栈空间的数据在函数结束都就释放了,在堆空间中的东西,函数结束后还会存在,需要用户手动free,要不然函数结束后他还会存在。如果你想把这块内存在功能块之外用的时候,请用malloc,而不能用动态内存。不管这块内存多小
4、普通项目中,确实直接用系统动态分配内存空间,足够了,因为现在一般项目中M0,M3的内核基本很少遇到内存不足的情况,内存不足,换个大内存的同类型= =!

int main()
{
	int* p = malloc(4 * sizeof(int));
	int i = 0;
	
	if( p != NULL)
	{
		for(i=0; i<4; i++)
		{
			p[i] = i * 10;
		}
		for(i=0; i<4; i++)
		{
			printf("%d\n", p[i]);
		}
		free(p);
	}
	return 0;
}

在这里插入图片描述

多元指针,指向指针的指针

Type V;
Type* pv = &v;
Type** ppv = &pv;
Type*** pppv = &ppv;

int a = 0;
int b = 1;

int* p = &a;
int** pp = &p;

**pp = 2;  // a=2
*pp = &b; // p=&b
*p =3 ; // b=3

一个样例函数:

#include<stdio.h>
#include<stdlib.h>

int getDouble(double** pp, unsigned n)
{
	int ret = 0;
	double* pd = malloc(sizeof(double) * n);
	
	if(pd != NULL)
	{
        printf("pd = %p\n",pd);
		*pp = pd;
		ret = 1;
	}
	return ret;
}

int main()
{
    double* p = NULL;

    if(getDouble(&p, 5))
    {
        printf("p= %p\n",p);
        free(p);
    }
	return 0;
}

在这里插入图片描述

二维数组

在这里插入图片描述

int main()
{
	int b[][2] = {1,2,3,4};
	int(*pnb)[2] = b;  // b的类型是 int(*)[2]

	*pnb[1] = 30;

	printf("b[0][0] = %d\n",b[0][0]);
	printf("b[0][1] = %d\n",b[0][1]);
	printf("b[1][0] = %d\n",b[1][0]);
	printf("b[1][1] = %d\n",b[1][1]);
}

在这里插入图片描述
pnb指向了素组b, b是什么样子的数组呢?
b: 0 号元素是 [1 ,2 ] 1号元素是 [3 ,4 ]
所以 pnb[0] ——> [ 1, 2]
pnb[1] ——> [ 3, 4]
*pnb[1] 想通过地址来取内容, pnb[1]是一个素组,所以这个取得是pnb[1]数组的第0号元素,是[3 , 4]数组中的3,把3 变成30.

不同类型指针分析

  • int p; //这是一个普通的整型变量
  • int *p; //首先从P 处开始,先与*结合,所以说明P是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针;
  • int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组;
  • int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P是一个由返回整型数据的指针所组成的数组;
  • int (*p)[3]; //首先从P 处开始,先与*结合,说明P是一个指针,然后再与[]结合 ( 与"()"这步可以忽略,只是为了改变优先级 ),说明指针所指向的内容是一个数组,然后再与int结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针;
  • int **p; //首先从P开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int结合,说明该指针所指向的元素是整型数据,指向指针的指针
  • int p(int); //从P 处起,先与()结合,说明P是一个函数, 然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据 ;
  • int(*p)(int); //从P 处开始,先与*结合,说明P是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int结合, 说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
  • int *(*p(int))[3]; //从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int结合, 说明函数有一个整型变量参数, 然后再与外面的*结合,说明函数返回的是一个指针, 然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数

5、自定义数据类型

typedef关键字

typedef 关键字,常用做法 typedef unsigned int uint,所以以前我还认为使用起来就是 typedef 旧名字 新名字,实际上是错误的。

语法是: typedef Type NewTypeName

使用的步骤是 : 在传统的变量声明表达式里,用 新的 类型名替换变量名 ,然后把关键字 typedef 加在该语句的开头。

下面的例子演示 typedef 的用法步骤,便于记忆 typedef 的用法(其实还是有点不太明白):

1、int a; //第一步,正常的定义某类型的变量
2、int my_int; //第二步,用新的类型名字 my_int 替换 变量名a;
3、 typedef int my_int;//第三步,在第二步的前面加上 typedef ;//实际上使用只有第三步
上面这个例子很常用,但是用于函数指针
1、void (*p)(int a); //第一步,定义一个函数指针变量 p
2、void (*PF)(int a); //第二步,用新的类型名字 *PF 变量名p;
3、 typedef void (*PF)(int a);//第三步,在第二步的前面加上 typedef ;//

typedef 的用途:

  1. 使用 typedef 为现有的类型创建别名。常用的:typedef unsigned long int uint32;
  2. 定义机器无关的类型。 typedef float REAL; /typedef double REAL; 我的理解这里怎么和第一点一样,不是太明白??????
  3. 简化一些比较复杂的声明, 用的相对不多这点有点不是太明白??下面的例子
/*不使用typedef 简化复杂的声明,Callback函数声明如下*/
Callback(u8 *begin, void (*pFunCallback)(u8* pchar, u8 Len), bool state); 
/*使用typedef ,Callback函数声明如下*/
typedef void (*PFunCallBack)(u8* pchar, u8 Len);

Callback(u8 *begin,PFunCallBack pFun, bool state);//等价于上面

示例

#include "stdio.h"

typedef float(FArr5)[5]; // 为 float[5] 数组类型取新名字  FArr5代表 float[5]
typedef int(IFuncII)(int, int); //  为 int (int, int) 函数类型取新名字 IFunII 表示函数类型

typedef FArr5* PFarr;     //为FArr5 (float[5])这个数组类型的指针取新名字
typedef IFuncII* PIFuncII;  // 为 指向这个类型的函数 的 指针 取新名字

float g_arr[5] = {0.1, 0.2 ,0.3}; //这个数组类型就是  FArr5 类型

int add(int a, int b)  // add 函数类型就是  IFunII
{
	return a + b;
}
int main()
{
	FArr5* pa = &g_arr; // pa是指针,指针指向 FArr5 这个类型的东西,这个类型是float[5]数组类型
	IFuncII* pf = add;  // pf是指针,指针指向 IFuncII 这个类型的东西,是int (int, int) 类型的函数

    PFarr  npa = pa;
	PIFuncII npf =pf;

	int i =0;

	for(i=0; i<5; i++){
		printf("%0.2f\n",(*pa)[i]);  //小数点保留2位,对其后的数据进行四舍五入
		printf("%f\n",(*pa)[i]);      //默认输出,小数点保留6位
		printf("%10.2f\n",(*pa)[i]); //输出宽度为10个字符,右对齐,前补空格,小数点保留2位
		
	}
	printf("%d\n",pf(2, 3));
	printf("%d\n",npf(2, 3));

	return 0;
}

在这里插入图片描述

typedef 和 #define

典型案例分析

    typedef char* pStr1;
    #define pStr2 char* 
    pStr1 s1, s2;
    pStr2 s3, s4;  //char *s3,s4;

通过上面的定义, s1,s2,为char* 类型,s3 为char* 类型, 但是s4 为char 类型, 因为#define 只是进行了字符串的替换,在定义 s3 s4 的时候,看注释,会变成s3 是指针,s4 是char

结构体和联合体

结构体使用我就直接贴图了,我看的视频教程是 唐佐林老师的C语言基础视频教程,因为是免费的,所以想看原视频的网上就能直接找到

struct:
在这里插入图片描述
union:
在这里插入图片描述
在这里插入图片描述
union demo:

  #include <stdio.h>
    union data{
        int n;
        char ch;
        short m;
    };
    int main(){
        union data a;
        printf("%d, %d\n", sizeof(a), sizeof(union data) );
        a.n = 0x40;
        printf("%X, %c, %hX\n", a.n, a.ch, a.m);
        a.ch = 'd';
        printf("%X, %c, %hX\n", a.n, a.ch, a.m);
        a.m = 0x2059;
        printf("%X, %c, %hX\n", a.n, a.ch, a.m);
        a.n = 0x3E25AD54;
        printf("%X, %c, %hX\n", a.n, a.ch, a.m);
       
        return 0;
    }

在这里插入图片描述
union的典型应用 – 判断系统大小端

    #include <stdio.h>
    /*
    小端系统: 低位数据存储在地地址内存中
    大端系统: 低位数据存储在高地址内存中

       0 x 00 00 00 01

    小端系统: [0X01][ 0 ][ 0 ][ 0 ]
               低   --------->   高
    大端系统: [0][ 0 ][ 0 ][ 0X01 ]
              低   --------->   高
    */
    int isLittleEnd()
    {
        union 
        {
            int i;
            char a[4];
        }test ={0};

        test.i = 1;
        return (test.a[0] == 1); //返回1,小端系统
    }
    int main(){
        printf("System Endian:%d\n",isLittleEnd());
        return 0;
    }

在这里插入图片描述

在这里插入图片描述在这里插入图片描述
注意:无名结构体类型总是互不相同的类型(互不兼容)下面看例子:

int main()
{
	struct 
	{
		int a; 
		int b;
	} v1;
	struct 
	{
		int a; 
		int b;
	} v2;
	struct 
	{
		int a; 
		int b;
	} *pv;

	v1.a = 1;
	v1.b = 2;
	
	v2 = v1; //错误的!!!!

	pv = &v2;	//错误的!!!!
}

柔性数组

结构体中有素组变量,但是却没有指定长度

#include<stdio.h>
struct softArray
{
	int len;
	int array[]; //不占用存储空间,所以sizeof softArray 为  4个字节
};

int main()
{
    printf ("len = %d\n",sizeof(struct softArray));  //len = 4
    return 0; 
}

柔性数组的使用:

#include<stdio.h>
#include<malloc.h>

struct softArray
{
	int len;
	int array[]; //不占用存储空间,所以sizeof softArray 为  4个字节
};

struct softArray* create_soft_array(int size)
{
	struct softArray* ret = NULL;
    
    if( size > 0){
        ret = (struct softArray*)malloc(sizeof(struct softArray) + sizeof(int) * size);
        ret->len = size;
    }
    
	return ret;
}

void delete_soft_array(struct softArray* sa)
{
    free(sa);
}

void func(struct softArray* sa)
{
    int i = 0;

    if( NULL != sa ){
        for(i=0; i<sa->len; i++){
            sa->array[i] = i + 1;
        }
    }
}
int main()
{
    int i = 0;
    struct softArray* sa = create_soft_array(10);

    func(sa);

    for(i=0; i<sa->len; i++){
        printf("%d\n",sa->array[i]);   //输出 1 23456789 10
    }
    delete_soft_array(sa);

    return 0;
}

位域

现在用得不多,位域成员必须是整形,占用的位数不能超过类型宽度,当存储位不足时,自动启用新的存储单元,可以舍弃当前未使用的位,重新启动新的单元

struct Bits1
{
    int a:   16; //正确
    short b:  8; //正确
    char  c:  8; //正确
    float f: 32; //错误  
}

struct Bits2
{
    unsigned char a:  6; //正确
    unsigned char b:  6; //正确
    unsigned char c:  4; //正确
    unsigned char d:  9; //错误  
}

struct Bits3
{
    unsigned char a:  4; //正确
    unsigned char  :  0; //正确
    unsigned char b:  4; //正确   [ a(4bits)  | 4bits(空)  ]  [ b(4bits) |    ]
}

枚举类型(enum)

  • enum 是C语言中的 自定义类型 关键字
  • enum能够定义 整形常量集合类型
  • 第一个常量值默认为 0,后续常量在前一个值的基础上加 1
  • 可以任意对美剧常量指定整形值(只能是整形
  • enum 枚举类型变量就是 整形 sizeof() 等于 4 ,就是int 类型
  • enum定义的值是真正意义上的常量

6、头文件相关

  • 头文件中只做函数声明和变量声明(不做具体定义
  • 头文件中可定义数据类型(typedef,struct,union,enum
  • 类型定义必须在头文件中,仅类型声明无法创建变量

7、补充基本知识

负数 无符号数相关

数据类型的最高位用于标识数据的符号:

  • 最高位为1,表明这个数为负数

  • 最高位为0,表明这个数为正数

  • 计算机用补码标识有符号数,正数的补码为正数本身,负数的补码为绝对值各位取反+1

  • 无符号数默认为正数,没有符号位

  • 对于固定长度的无符号数
    最大值 + 1 = 最小值
    最小值 - 1 = 最大值

  • C语言中只有 整数类型 能够声明 unsigned 变量

  • 当无符号数与有符号数混合计算时,会将有符号数转换为无符号数后再计算,结果为无符号数

  • char类型 和 short类型运算,会隐式转化为int类型;

浮点数

  • int类型范围:[ -2^31 ~ 2^31 - 1]
  • float类型范围:[ -3.4E+38 ~ 3.4E+38]
  • int 和 float 都占 4个字节的内存,他们所能表示的数据的个数是一致的,但是float 比int 范围大得多, 表示的数据个数一致,所以float表示的数据不是连续的,所以说float是不精确的,同理 double也是类似,只是他比float 精度高一些

do 和 break的妙用:

#include <stdio.h>
#include <malloc.h>

int func(int n){
    int i = 0;
    int ret = 0;
    int *p = (int*)malloc(sizeof(int*) * n);

    do{
        if( NULL == p ) break;
        if( n < 5 ) break;
        if( n > 100 ) break;
        for(i=0; i<n; i++){
            p[i] = i;
            printf("%d\n",p[i]);
        }
        ret = 1;
    }while(0);
	printf("free(p)\n");
    free(p);
    return ret;
}
int main()
{
    if(func(1000))
    {
        printf("OK!\n");
    }else{
        printf("ERROR\n");
    }
    return 0;
}

void*

void* 类型的指针可以接受任意类型的指针值

const 和 volatile

const 修饰的变量不是真的常量,它只是告诉编译器该变量不能出现在赋值符号的左边

  • const 修饰的变量是只读的,本质还是变量
  • const 修饰的局部变量 在 栈 上分配空间
  • const 修饰的全局变量 在 全局数据区 分配空间
  • const 只在编译期有用,在运行期无用
  • 现代的C编译期中的const将 具有全局生命周期 的变量存储于只读存储区(比如gcc,看下面例子,编译通过,但是执行会出错,) static 修饰的变量也具有全局生命周期
  • const 修饰的普通 局部变量 是在栈上分配空间的,所以下面的例子成功修改了局部变量cc的值
  • 对于不该被修改的入参,应该用const修饰,这是const使用的常见姿势。
  • const修饰的变量只能正常赋值一次。
  • 不要试图将const数据的地址赋给普通指针。
  • 虽然可以通过某种不正规途径修改const修饰的变量,但是永远不要这么做。
#include<stdio.h>
const int g_cc = 2;

int main()
{
    const int cc =1;
    int *p = (int*)&cc;

    printf("cc= %d\n",cc);
    
    *p = 3;
    
    printf("cc= %d\n",cc);
    printf("g_cc= %d\n",g_cc);

    p =(int*)&g_cc;

    *p = 4;
    printf("g_cc= %d\n",g_cc);
    return 0;
}

在这里插入图片描述

  • volatile 可理解为 “编译期警告指示字”
  • volatile 告诉编译期必须每次去内存中取变量值
  • volatile 主要修饰可能被多个线程访问的变量
  • volatile 也可以修饰可能被未知因素更改的变量
int ob = 10;
int a = 0;
int b = 0;
a = ob;
delay(100);//如果ob在其他线程或者中断中修改了值,就会出问题
b = ob;
const volatile int i = 0;//定义了一个只读变量i,每次都得去内存中取值,不让编译期进行优化

在这里插入图片描述

sizeof

sizeof 是C语言的内置关键字而不是函数

  • 在编译过程中所有的 sizeof 将被具体的数值所替换
  • 程序的执行与 sizeof 没有任何的关系
int var = 0;
int size = sizeof(var++);
printf("%d, %d\n",var,size); // 0,4

int func()
{
	printf("sdasdasda\n");
}
int main()
{
	size = sizeof(func()); //这个不是调用函数,所以func()中的打印不会执行
	printf("%d\n",size); // 4
} 

逻辑运算符分析

  • || 从 左向右开始计算

    当遇到为 的条件时 停止计算,整个表达式为

  • && 从 左向右开始计算

    当遇到为的条件时停止计算,整个表达式为

  • 逻辑表达式中,虽然 && 比 || 具有更高的优先级,但是并不是说 && 先计算,而是如下解释:
    在这里插入图片描述

  • 运算优先级: 四则运算 > 位运算 > 逻辑运算 (实际项目中用括号)

#include <stdio.h>

int main()
{
    int i=0,j=0,k=0;

    ++i || ++j && ++k;

    printf("%d\n",i);
    printf("%d\n",j);
    printf("%d\n",k);

    return 0;
}

在这里插入图片描述

++ 和 - - 问题

  • ++ 和 – 参与混合运算的结果是不确定的,编译器不同,可能结果不同
  • 在混合运算中, ++ 和 – 的汇编指令可能被打断执行
int i = 0;

(i++) + (i++) + (i++);

(++i) + (++i) + (++i);
/*求i的值和表达式的结果,在不同编译器下运行的结果不一样,实际项目不要这么写*/

在这里插入图片描述
所以实际项目中 多使用 空格 是个好习惯

空格可以作为C语言一个完整符号的休止符,编译器读入空格后立即对之前读入的符号进行处理

int main()
{
	int i = 0;
	int j = ++i+++i+++i; //编译器一直读,读到 ++i还会继续 读到++i+ 编译器继续往下读,\
						   如果读到一个变量,还会继续读,但是编译器读到++i++,编译器认\
						   为不管下面读到什么,也不能组成有意义的符号,所以读到++i++就\
						   停止去处理,处理后就是 ++i++ => 1++; 所以这里就出问题了,\
						   ++i + ++i + ++i;加上空格解决
	int a = 1;
	int b = 4;
	int c = a+++b;//编译器读到 a+++b; 能够一直读完再处理  a++ + b;
	
	int* p = &a;
	b = b/*p;   //这里写法也会被编译器误认为后面是注释,编译器一直读,打一个空格 \
				  b = b / *p; 就能够正常
}

8 、一些符号

反斜杠

C语言的转义字符(\)主要用于标识无回显字符,也可用于表示常规字符
在这里插入图片描述

#include <stdio.h>

int main()
{
    char enter = '\n';      //  换行
    char* p = "\141\t\x62"; // \转义字符  将8进制的 141 转成10进制就是 97 然后\t为 tab 按键 16进制的62 就是 98

    printf("%s",p);
    printf("%c",enter);

    return 0;
}

在这里插入图片描述

  • C语言中的反斜杠(\)同时具有 接续符 和 转义符 的作用
  • 作为接续符使用时可直接出现在程序中
  • 作为 转义符 使用时需出现在 单引号或双引号 之间

单引号和双引号

  • C语言中的 单引号 用来表示 字符字面量

    字符字面量 编译为 对应的 ASCII码

  • 单引号括起来的单个字符代表 整数

  • C语言中的 双引号 用来表示 字符串字面量

    字符串字面量 编译为 对应的 内存地址

  • 双引号括起来的字符代表 字符指针

  • ‘a’ 表示 字符 字面量
    在内存中占一个字节
    ‘a’ + 1 表示 ‘a’ 的ASCII码加1,结果为 ‘b’

  • “a” 表示 字符串 字面量
    在内存中占2个字节
    “a” + 1 表示 指针运算,结果指向 “a” 结束符 ‘\0’

  • printf 的第一个参数被当成字符串内存地址

  • 内存的低地址空间不能再程序中随意访问
    在这里插入图片描述在这里插入图片描述

#include <stdio.h>

int main()
{
    char c = " ";//c一个字节,后面的是一个地址值4个字节,赋值给c会产生截断     

    while((c == "\t") || (c == " ") || (c == "\n"))//双引号中的值编译过后是一个地址值
    {
        scanf("%c",&c);
    }
    return 0;
}

在这里插入图片描述
分析上面的例子:

char c = “string”;

  1. 编译后字符串 “string” 的内存地址被赋值给变量 c;
  2. 内存地址占用 4个字节,而变量 c只占用一个字节;
  3. 由于类型不同,赋值后产生截断。

所以上面的例子,while语句一次也不会执行。
所以上面的例子,正确的是把所有的双引号变成单引号;变量c和单引号内对应字符的 ASCII码进行比较,变量c本身赋值为空格键的 ASCII码对应的值。

9、编译过程

在这里插入图片描述在这里插入图片描述

预编译(预处理器)

  • 处理所有的注释,以空格代替

  • 将所有的 #define 删除,并且展开所有的宏定义

  • 处理条件编译指令 #if, #ifdef, #elif, #else, #endif

  • 处理 #include, 展开被包含的文件

  • 保留编译期需要使用的 #pragma指令

  • 预处理指令示例:
    gcc -E file.c -o file.i

编译(编译器)

对预处理文件进行词法分析,语法分析和语义分析

  • 语法分析:分析关键字,标识符,立即数等是否合法
  • 语法分析:分析表达式是否遵循语法规则
  • 语义分析:在语法分析的基础上进一步分析表达式是否合法

分析结束后进行 代码优化 生成相应的 汇编代码文件

  • 编译指令示例:
    gcc -S file.c -o file.s

汇编

  • 汇编器将汇编代码转变为机器的可执行指令
  • 每条汇编语句几乎都对应一条机器指令
  • 汇编指令示例:
    gcc -c file.c -o file.o

链接

链接是指 将目标文件最终链接为可执行程序

  • 静态链接:目标文件 直接连接进入可执行程序(小工程项目常用方式)
  • 动态连接:在程序 启动后在动态加载目标文件(大型软件系统,方便更新局部功能,使用很多动态库,然后更新的时候,只需要更新相应的库文件,程序运行过程中加载的库文件)

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
动态库使用示例:

#include <stdio.h>
#include <dlfcn.h>

int main()
{
	void* pdlib = dlopen("./dlib.so", RTLD_LAZY);
	
	char* (*pname)();
	int (*padd)(int, int);

	if(pdlib != NULL)
	{
		pname = dlsym(pdlib,"name");
        padd = dlsym(pdlib,"add");

        if((pname != NULL) && (padd != NULL))
        {
            printf("Name: %s\n",pname());
            printf("Result: %s\n",padd(2, 3));
        }

        dlclose(pdlib);
	}
    else
    {
        printf("Cannot open lib ...\n");
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

矜辰所致

您们的鼓励是我奋斗的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值