C语言程序设计(完整版)

 基础数据类型

  • 整数 signed(默认/缺省)/unsigned 定点数

    • 短整型 short 2byte

    • 整型 int 4byte

    • 长整型 long 4/8 byte

    • 长长整型 long long 8byte

    • 字符 char 1byte

  • 浮点数

    • 单精度浮点类型

      • float 4byte

    • 双精度浮点类型

      • double 8byte

    • 高精度浮点类型

      • long double 12/16byte

        类型字节大小取值范围取值范围
        [signed] char1byte[-128,127]-2^7^,2^7^ -1
        unsigned char1byte[0,255]0,2^8^ -1
        [signed] short [int]2byte[-32768,32767]-2^15^,2^15^-1
        unsigned short [int]2byte[0,65535]0,2^16^-1
        [signed] int4byte-2^31^,2^31^-1
        unsigned int4byte0,2^32^-1
        [signed] long [int]4/8byte-2^31^,2^31^-1 / -2^63^,2^63^-1
        unsigned long [int]4/8byte0,2^32^-1/0,2^64^-1
        [signed] long long [int]8byte-2^63^,2^63^-1
        unsigned long long [int]8byte0,2^64^-1
        float4byte+-3.4*10^38^+- 2^128^ - 2^104^
        double8byte+-1.79*10^308^+- 2^1024^ - 2^971^
         

      • 计算机存储数据

        • 二进制 整数与二进制转换

          • 整数转二进制 除2取余

          • 小数转二进制 乘2取整

        • 原码/反码/补码/移码 原码/反码/补码

          • 正数 原码 == 反码 == 补码

          • 负数 反码 = 其原码符号位不变,其它位按位取反

            补码 = 其原码符号位不变,其它位按拉取反+1

        • 整数

          • 计算机以补码形式存储整数

        • 浮点数

          • 计算机以IEEE754标准存储小数(float/double/long double)

  • 浮点数存储
    • IEEE754标准

      value = sign x exponent x fraction
                     (exponent)max = 127
                     (fraction)max = .111111111111111111111(23个1) 
                     1.111111111111111111111(23) x 2^127
                     = (1+1/2+1/4+1/8+  1/2^23) x 2^127
                     = (2-1/2^23) x 2^127
                     = 2^128 - 2^104
      ​
      3.25   按正常的转换为二进制
          0011.01   ==>   1.101 x 2的1次方
          xxxx.xxxx ==>   1.xxxxxxx  * 2^n次方
          存储:
              符号位:   1bit     0正/1负
              n次方:    指数偏移数    1 + 2^(exponet的二进制位-1)-1
                       指数   以   移码形式存储
              1.xxxxx:  小数位          小数点前固定1,只存储尾数
          [0]      [0000 0001 + 2^(8-1)-1]  [101]
                   [0000 0001 + 0111 1111]
                   [1000 0000]              [101  (20个0)]
          sign    exponent                   fraction
          0100 0000 0101 0000 0000 0000 0000 0000
          0100 0000 0101 0000 0000 0000 0000 0000
      • float == 4byte === 32bit

        • sign 1bit

        • exponent 8bit

        • fraction 23bit

      • double == 8byte ==== 64bit

        • sign 1bit

        • exponent 11bit

        • fraction 52bit

    • 定点数与浮点数

      • 定点 小数点固定,在末尾 整数

      • 浮点 小数点位置不固定,是浮动 小数

-567.1  
567-512 = 55-32=23 -16=7
01000110111.0001100110011001100110011001100110011....
0.1x2 = 0.2 x2 = 0.4x2=0.8x2 = 1.6  0.6x2=1.2  0.2x2=0.4
01000110111.0001100110011001100110011001100110011....
==>  1.0001101110001100110011001100110011001100110011....*2^9
==>  1100 0100 0000 1101 1100 0110 0110 0110
     1100 0100 0000 1101 1100 0110 0110 0110

 

  • 字符

    • 本质是整数

    • char是1byte

    • 但是字符字面值 是 4 byte

    • 字符以二进制方式存储

    • ASCII码表

      • 48 === '0'

      • 65 === 'A'

      • 97 === 'a'

    • 字面值字符

      'a'    '0'       'b'    'A'  'Z'
      转义字符    '\n'   '\r'    '\b'   '\t'   '\0'
      八进制字符  '\101'   '\102'        '\ooo'   ooo八进制一位
      十六进制字符  '\x41'               '\x##'   ## 十六进制一位
  • 编码

    • ASCII码

    • Unicode编码

    • gbk

    • utf-8

  • 浮点数

    • 表示一个不精确的值,近似

    • inf -inf 正无穷/负无穷

      • 指数位全为1,尾数全为0

    • nan 无效的浮点数

      • 指数位全为1,尾数不全为0

    • 0 指数和尾数全为0

    • 指数位为什么要用移码

      • 方便比较大小 (移码二进制从左往右,哪个数先遇到1哪个数大)

      • 移码如何计算 取决于 二进制位数

        • 移码 = 补码 + (2^(二进制位数-1)^-1)

  • 数据的本质就是0和1

 

变量

定义变量
  • 创建(从无到有)变量的语句

  • 同一个作用域,不可以定义同名的变量

    数据类型  变量名;
  • 在定义变量时可以初始化/初始值

  • 定义变量意味着创建

    • 分配内存 数据类型必须是明确的

    • 分配多少个内存,取决于 sizeof(数据类型)

声明变量
  • 声明变量已经定义了,让编译器不要报错

  • 告诉编译器该变量在其它文件中/作用域中已经定义过了,声明之后在当前作用域可以使用

  • 声明变量,可以重复声明同一个变量

    extern 数据类型 变量名;
  • 声明变量不可以给初始值

根据定义变量的位置,对变量进行分类
  • 全局变量

    • 在全局作用域定义的

    • 存储在 全局数据区

    • 如果全局变量没有初始化 自动初始化为0

  • 局部变量

    • 在函数内部定义的

    • 普通局部变量(除了static) 存储在 栈区

    • 普通局部变量,如果没有初始化 垃圾值

  • 块变量

    • 块变量也可以称为局部变量

    • 在语句块中的定义的变量

变量存储限制修饰符
  • auto

    • 自动的 默认

  • static

    • 静态的

    • 和auto相对

    • static修饰全局变量

      • 改变全局变量的作用域,static修饰的全局变量只能在当前文件中作用

    • static修饰局部变量

      • 改变局部变量的存储位置,普通局部变量存储在栈区,static修饰的局部变量存储在全局数据区

  • register

    • 寄存器

    • 申请把变量作为寄存器变量 ,变量直接存储在寄存器上

    • 不可以对register修饰的变量进行取址运算&

    • 在编程过程中,程序员不会把一个变量定义为register,但编译器有时候会优化

  • volatile

    • 易变的

    • 变量的值可能会发生意想不到的变化

    • 多线程场景中,多个线程对同一个变量进行操作,volatile修饰的变量,在对其进行访问时,不会直接获取其寄存器上面的值,而是需要重新从内存中加载

    • CPU在处理数据时,如果发现这个数据在寄存器上面已经有了,直接使用,但是如果是volatile,即使寄存器上有,它也会要求重新从内存中加载

 

类型修饰符
  • const

    • 表示只读,认为是常量

    • const与指针

  • extern

    • 外部的,声明外部变量

  • 在同一个作用域下,不能定义相同名字变量

  • 如果在不同的作用域下,是允许定义相同名字的变量

    int num = 1024;
    
    int main(){
    	int num = 9527;
    	printf("num = %d\n",num); //这里访问到是 局部优先原则    同名的全局变量被隐藏
        //如何在此处访问到被隐藏的全局变量
        {
            extern int num;    //声明外部变量
            printf("num = %d\n",num);  //1024
        }
    	return 0;
    }
  • 取变量中存储的值

  • 对变量进行赋值

 

操作符

算术运算符

+
-
*
/
%
​
++
--
  • 注意点

    • % 取余 只适合于整数(char)

    • / 整数不能除以0(浮点数除外)

    • ++/--

      • 前++/-- 在表达式先自增/减,然后参与运算

      • 后++/-- 先取值进行运算,然后自增/减

      • 在同一个表达式,尽量避免对同一个变量进行多次自增/自减

      • 在调用函数传递参数时,避免对传递进行自增/减,特别是宏函数

关系运算符

>
>=
<
<=
==
!=
  • 注意

    • 关系表达式的结果为0/1

    • 多个关系表达式需要用逻辑运算符连接

      int a = 0;
      scanf("%d",&a);
      if(0 < a < 10){  //恒成立    (0<a) < 10    0<a的结果要么是0,要么是1
          printf("a在(0,10)区间\n");
      }
      ​
      if(0 < a && a < 10){  //判断a是否在(0,10)区间
      ​
      }

    • 判断相等,不要写成赋值

      if(a == 10) ==>   if(10 == a)

逻辑运算符

&&
||
!
  • 数据与逻辑值

  • 在逻辑领域,非"零"即真,真即"1"

    int a = 10,b = 20;
    if(a && b){  //10 && 20     这里面的10和20逻辑真
    ​
    }

  • 逻辑表达式的结果是 0或者1

  • 短路特性

    • 0&&++i;

      • 逻辑与&&运算符,&&前面表达式如果为假(0),则后半表达式不运算

    • 1||++i;

      • 逻辑或||运算符,||前面表达式如果为真(1),则后半表达式不运算

    • 后面部分结果影响不了表达式最终的结果,短路径

  • 逻辑&&运算符优先级 高于 逻辑||

    a || b && c     ===>   a || (b && c)
    a && b || c     ===>   (a && b) || c

位运算符

&
|
^
~
>>
<<
  • 只适用于整数

  • &|^ 相同位置的二进制位进行运算

    num & -1   ==  num
    num | -1   ==  -1
    num | 0    ==  num
    num ^ 0    ==  num
    num ^ -1   ==  ~num  == -(num + 1)
        num 如果是正数   -num = ~num + 1

  • 右移分情况

    • 有符号的数 左边空出的位全部补符号位

    • 无符号的数 左边补0

  • 左移

    • 右边空出的位补0

  • 右移1位相当于除以2 (负奇数除外 n/2-1)

  • 左移1位相当于乘以2

赋值运算符

=
+=  -=  *=  /=   >>=  <<=   &=  |=  ^=    &&=   |=
  • 数据溢出

    • 整数数据,宽字节整数赋值窄字节变量时,只截取低字节数据,多出部分舍弃

    • 浮点数赋值整数,直接舍弃小数部分,且整数也可能溢出

  • 数据填充

    • 整数数据,窄字节数据赋值给宽字节变量时,需要在高位填充

      • 窄字节数据如果是signed类型,则高位填充符号位

      • 窄字节数据如果是unsigned类型,则高位填充0

  • 练习(嵌入式必备)

    • 对于int类型的变量num

    • 在不影响其它二进制位的情况下

      • 假设从最低位编号 1 到最高位32

      • 把2-5 置1 num = num | (0xf<<1);

      • 把8-15 置0 num = num & ~(0xff<<7);

      • 把20-23 取反 num = num ^ (0xf<<19);

三目运算符

?:
expr?res1:res2
​
if(expr){
    return res1;  //res1  可以是一个语句
}else{
    return res2;  //res2  可以是一个语句
}

取址/解引用运算符

&     获取变量的存储地址         虚拟内存   编号      %p     十六进制的整数
*     解引用                   *(指针/内存)    取得内存地址中的数据
如果内存/指针的类型为  void *    则不能解引用
  • 取址运算符,操作数必是存储在内存中数据对应的变量

下标运算符

[]
​
arr[index]    <==>     *(arr+index)
  • 本质是指针偏移解引用

  • 下标运算符的操作数可以交换

    arr[i]   <===>   i[arr]

sizeof

  • sizeof是操作符,不是函数

  • sizeof操作数可以是类型,也可以是变量,表达式,语句

    • 操作数如果是类型,必须要有小括号 sizeof(类型)

    • 操作数如果不是类型名,则()可省 sizeeof ret

  • sizeof只关心操作数的类型,不会计算结果

    • sizeof(表达式) 表达式不会运算

    • sizeof(函数(参数)) 不会调用函数,只关心函数返回值类型

    • sizeof(函数名) == 1 C语法规定,未定义的结果

void func(int arr[],int len){     //arr    <===>  int *arr
    printf("%lu\n",sizeof(arr));  //4/8
}
​
void func1(int arr[10]){          //int arr[10]   形参列表   int *arr
    printf("%lu\n",sizeof(arr));  //4/8
}
int main(){
    int arr[10] = {};
    printf("%lu\n",sizeof(arr));  //40
    func(arr,10);
    return 0;
}
sizeof("pointer");   //8   包含'\0'
char *s = "pointer";
sizeof(s);           //4/8    求指针变量s的内存空间大小
​
sizeof("ab");       //3
char *s1 = "ab";
sizeof(s1);         //4/8
​
​
char s2[] = "hello";
sizeof(s2);        //6
char s3[10] = "hello";
sizeof(s3);        //10
​
sizeof('a');       //4
char ch = 'a';
sizeof(ch);        //1
    
sizeof(long) 4/8

sizeof(long double) 12/16

sizeof(指针) 4/8

成员访问/间接成员访问运算符

.
->
​
结构体变量.成员名
​
结构体指针->成员名

其它

,
()
(x,y,c,a+b,c+d,w*y)    从左往右依次计算,最终的结果取最后一个表达式的结果

运算符的优先级

  • 逻辑非 > 算术运算符 > 关系运算符 > 逻辑&& > 逻辑|| > 赋值运算符

  • 位运算符~ > 算术运算符 > 移位(>>/<<)

char ch = 188;
int num = ~ch >> 1 + 2;
​
unsigned ch1 = 188;
int num = ~ch >> 1 + 2;

数据提升

  • 在混合类型运算时

    • char 会自动提升为 int类型

    • unsigned char ---> unsigned int

    • short 会自动提升为 int类型

  • 在有符号和无符号数据进行运算符时

    • 有符号 --> 无符号

语句

  • 分支选择

    • if

      if(cond)
      {
      ​
      }
      ​
      if(cond)
          只有一条语句属于if分支控制的语句;
      ​
      ​
      if(cond){
      ​
      }else{
      ​
      }
      ​
      //同一个分支语句  在某一次运行过程中,最多有一个分支被执行
      //条件是从上往下依次判断的,如果有条件为真,则进入对应的分支执行,后面的条件将不再执行   如果某一次执行过程中,执行了condn条件判断语句,则说明前面所有的条件都为假
      if(cond1){
      ​
      }else if(cond2){
      ​
      }....
      ​
      ....
      else if(condn){
      ​
      }
      ​
      ​
      if(cond1){
      ​
      }else if(cond2){
      ​
      }....
      ​
      ....
      else if(condn){
      ​
      }else{  //能够保证一定会有一个分支被执行到
      ​
      }
      ​
      ​

    • switch

      switch(整型表达式){  //switch(表达式的结果必须为整数类型(char))
          case 整型常量表达式:  // case 结果必须是整型, 而且必须是常量
          
          //在同一个switch中,case 整型常量表达式的结果不能相同
          
          //break 语句不是必须品  可选项   
          //case 作为入口,当进入之后,顺序往下执行,并且忽略下面的case和default,遇到break就跳出
          //default  没有位置限制   可以放在任意case的前后
          //default虽然没有位置限制,但一定是在所有的case匹配之后,都没有相等的才会执行default分支, default也是可选项
          case n ... m:   //仅限于  GNUC                 
      }
      • 常量表达式在编译时就会计算出结果

  • C语言标准

    • ANSI C 美国标准委员会C语言标准

    • ISO C 国际标准委员C语言标准 直接抄ANSI C

    • GNU C GNU C语言标准 GNU 组织的名称/linux下面一个开源软件组织

      • 扩展了ANSI C语言的语法

  • 常量

    • 字面值常量

      整数字面值常量
      1   1024   9527   -100    0xABF   0X18D     077   045
      7L    88UL    100ll     110ull
      浮点数字面值常量
      3.14     0.333    .789
      1.1(默认是double类型)
      1.1f          2.2F
      3e7            1.1e8        2E1
      字符字面值
      'a'  '0'  'A'  'Z'
      '\n'  '\b'  '\r'  '\\'  '\\'
      '\101'  '\102'
      '\x41'  '\X42'
      字符串字面值
      "hello"   "world"   "  "
    • 宏常量

      #define N 10
      #define M 19+1

    • 枚举值

      enum DIR{UP,LEFT,DOWN,RIGHT};
      //UP,LEFT,DOWN,RIGHT  常量

    • const修饰的变量,不是真正的常量

      • const常量 ,并不是真正的常量

  • 条件循环

    • for

      for(init;condition;change){
          forbody;
      }
      ​
      init  一次运行中,有且只会执行一次   可以为空
      condition  条件判断,为真,则执行forbody循环体,为假结束循环  
                 condition为空,恒为真
      change   改变循环变量的值  允许为空
      ​
      无限循环
      for(;;){}   for(;1;){}
      ​
      for(;condition;);
      {
          语句块不是循环体的内容
      }
      ​
      for(init;cond;change);    //循环体为空
      ​
      ​
      C99标准中,允许init中定义循环变量

    • while

      init
      while(cond){
          forbody;
          change;
      }
      ​
      cond 不能为空

    • do while

      do{
      ​
      ​
      }while(cond);
      //do while循环体至少执行一次

  • break 和 continue

    • break结束循环

    • continue 结束本次循环[continue语句下面的内容不执行了],进行下次循环

  • 循环嵌套

  • 跳转

    • goto

      goto lable;    //跳转到指定标签处执行代码
    • 比较危险,建议减少使用

函数

  • 函数是满足特定语法的代码段

  • 功能: 函数可以被重复调用 提高代码复用 提高开发效率 减少代码冗余

  • 分类

    • 标准库函数

      • 函数声明放在标准库头文件中,直接#include头文件,直接调用即可

    • main函数

      • 一个C语言程序有且只有一个main函数,C语言程序的入口函数

      • main函数的参数

        int main(int argc,const char *argv[]){
             
        }

    • 自定义函数

      • 函数的定义

        ret-type function-name (argument-list)
        {
            function-body;
        }
        • 四要素

          • 返回值类型

            • 如果一个函数没有返回数据,则定义为void

          • 函数名

            • 标识符要求 同一个作用域不同名

            • 函数名 即 内存地址

              printf("%p\n",funcname);
              printf("%p\n",&funcname);
              printf("%p\n",*funcname);
              printf("%lu\n",sizeof(funcname)); //1
              printf("%lu\n",sizeof(funcname(a)));//sizeof(ret-type)

          • 参数列表

            • 形参 只能在本函数内部使用

            • 在函数调用时,形参创建,函数调用结束之后,形参消失

            • 形参名可以和实参名相同,但它们不是同一个

            • 形参之间用逗号隔开

            • 可变长参数列表

              printf(const char *format,...);
            • func(void) 和 func() 区别

          • 函数体

            • {}不能省

            • 代码段

            • return语句,与ret-type对应

              void func(){
                  ...
                  return;
                  ...
              }

            • return a,b,c;

            • 如果函数返回值类型为指针,则需要注意,不要返回局部的内存地址(栈内存),函数调用之后,内存将被释放(可能延迟)

      • 函数声明

        [extern] ret-type function-name(argument-list);
        extern可省
        argument-list 形参名可省,只需要类型即可
        • 函数可以重复声明,但不可以重复定义

        • 遵循先声明(定义)后使用的原则

        • 函数的隐式声明

          • 当编译器编译到代码调用了一个函数时,如果该函数已经声明或者定义过,则会检查其函数类型是否一致,如果没有声明和定义,编译器不会直接报错,它会隐式声明一个函数,并将该隐式声明的函数声明为int类型的返回值,当最终 链接阶段,如果发现没有该隐式声明的函数,则会报错,如果有,则会比对类型,可能报错或者警告

    • 第三方库函数

      • 包含编译好的库 和 头文件

  • 递归函数

    • 函数自己调用自己

    • 退出条件

    • 递归"公式"

  • 函数调用

    ret = function(argumentlist);
    ​
    //在函数定义时,形参列表是有类型的  function(int a,int b)
    //在调用函数时,传递的参数称为实参  function(a,b)
    • 函数调用,本质是跳转指令,跳转到函数名(内存地址)执行机器指令

    • 函数调用,需要在栈内存中分配内存以供函数调用时的内存开销,譬如形参列表,局部变量,当函数调用结束之后,回收在栈内存中开辟的内存空间

  • 参数传递的几个问题

    • C语言函数的参数传递是单向的值传递

    • 即,将实参给形参初始化

    • 实参和形参 仅仅是 在调用时 值相等 他们是不同的变量

      void func(int num){
          num = 9527;
      }
      ​
      int main(){
          int num = 1024;
          func(num);  //  int func::num = num;
          //num还是等于1024
          return 0;
      }
      
      void func(int *pnum){//int *pnum = &num;
          *pnum = 9527;
      }
      ​
      int main(){
          int num = 1024;
          func(&num);
          return 0;
      }

数组

  • Array数组是有限的有序元素集合

  • 数组元素的顺序集合

  • 一个数组中所有元素的类型是相同的

  • 数组的长度即数组元素的个数

  • 数组中的元素通过下标来进行访问的

一维数组
定义
Type ArrayName[ARR_LEN];
  • Type类型,即数组中存储元素的类型

    • 定义数组,就相当于定义了若干个同类型的变量

  • ArrayName 数组名 ,标识符

    • 数组名是一个常量(常量不能单独放在 = (赋值运算符) 左边)

      int arr[5];
      arr = {1,2,3,4,5};  //错误的
      sizeof(ArrayName)         -- 整个数组所占内存的大小

    • sizeof(ArrayName)/sizeof(ArrayName[0]) 得到数组的 长度

  • ARR_LEN 即数组长度 元素的个数 变量的个数

    • 必须是非负整数

    • 如果ARR_LEN是整型常量表达式,则可以直接初始化

    • 如果ARR_LEN不是常量,则不能直接初始化( = {})

      • 称为 可变长数组

        • 可变长数组不是指数组长度可以变化 ,而是指定义该数组中,数组长度是一个整型表达式(非常量)

      • 可变长数组不能在全局作用域中定义,可变长数组也不能声明为static

    • 如果在定义数组中ARR_LEN不指明,则必须直接初始化

      int arr[] = {1,2,3,4,5};
      • 数组的长度取决于初始化器{}中元素的个数

    • 数组一旦定义,数组长度不可以改变

初始化
  • 初始化器{元素,元素,...}

    int arr[5] = {1,2,3,4,5};
    int brr[5] = {1,2,3,4,5,6};        //编译警告
    int crr[5] = {1,2};                //后面补0
    int drr[5] = {0};                  //全部为0
    int err[5] = {};                   //全部为0
    int frr[5] = {[2]=1,[4]=2};        //指定初始化 其余0
    int grr[] = {1,2,3,4};
    int n = 0;
    scanf("%d",&n);
    int hrr[n]; 
    ​
    void func(int n){
        int arr[n];
    }

数组元素访问
  • 数组中的元素是通过下标访问的

    ArrayName[index]
    ​
    index --  整型表达式

  • 通过下标,可以访问数组中的元素,也可以修改数组中的元素的值

  • 下标运算符[],本质是指针偏移解引用

    ArrayName[index]         ===     index[ArrayName]
    *(ArrayName + index)

  • index下标从0开始,到 ARR_LEN -1 (数组长度-1)

    • 如果超过[0,ARR_LEN-1] 范围,则数组越界访问,非常危险的

  • 一维数组作为参数传递

    void func(int arr[],int len){  //(int *arr,int len)
        
    }
    int arr[10];
    func(arr,10);  //传递数组名和数组长度

再议数组名
  • 数组名是常量,

  • 通过sizeof(数组名) == 得到数组内存大小

  • 数组名 内存地址

    &arr[0]          数组第一个元素的内存地址   首元素内存地址
    &*(arr + 0)     ==    &*arr     ==    arr
    • 数组名即数组首元素内存地址

    • 在调用函数把数组名作为实参传递时,事实上传递了 &arr[0]数组第一个元素的内存地址

    • 所以,在形参列表中的 数组,本质就完全退化为指针,这个时候sizeof(形参数组) == sizeof(指针)

  • 数组名 以及 对数组名取址

    &arr[0]
    arr
    &arr
    ​
    以上三个值相等,区别在于类型不相等
        &arr[0]和arr  类型为      TYPE *
        &arr          类型为      TYPE (*)[ARR_LEN]
        
    &arr[0]+1   和 arr+1     实际 + sizeof(TYPE)
    &arr+1                   实际 + sizeof(arr)

二维数组
  • 二维数组可以理解为行列矩阵,但实际上二维数组依然是连续的有序元素集合

  • 二维数组 本质上 是一维数组 ,只是一维数组中的元素是 一维数组

定义
TYPE ArrayName[ARR_LEN1][ARR_LEN2];
​
ARR_LEN1   即二维数组中的长度             行
ARR_LEN2   即二维数组中元素(一维数组)的长度 列
初始化
int arr[4][5] = {{1,1,1,1,1},{2,2,2,2,2},{3,3},{4,4}};
int brr[3][4] = {1,2,3,4,5,6};
int brr[4][5] = {[1] = {1,2,3,4},[2][3]=1,[3][0]=5};
​
int crr[][3] = {1,2,3,4,5,6,7};  //3
int drr[][3] = {{},{1,2},{1},{}}; //4
  • 二维数组在定义时,一维长度不能省

  • 二维数组的二维长度 = sizeof(ArrayName)/sizeof(ArrayName[0])

  • 二维数组的一维长度 = sizeof(ArrayName[0])/sizeof(ArrayName[0] [0])

二维数组中元素的访问
  • 通过两个下标进行访问

    ArrayName[二维下标][一维下标]
    ArrayName[行][列]
    ​
    int arr[4][5] = {};
    int ret = arr[1][7];   //数组有越界吗?
    ​

  • 二维数组作为参数传递

    void func(int (*arr)[10],int len){
        
    }
    ​
    void goo(int (*arr)[],int row,int col){
        int (*brr)[col] = arr;//这样才能用
    }
    int arr[5][10];
    func(arr,5);
    int brr[6][8];
    ​
    goo(arr,5,10);
    goo(brr,6,8);

指针

  • 指针即内存地址

  • 指针变量 即 存储内存地址的变量

  • 内存地址

    • 程序当运行起来,会被操作系统加载内存中,形成内存映像(内存空间)

    • 对内存空间进行编号 (0x0 0x1 0x2 ... )

    • 内存编号 (整数,%p 十六进制的整数)

取址运算符 &
  • 获取变量的内存地址(内存编号)

  • 可以使用%p来进行格式化输出

  • &操作数必须是标识符

    • 变量名

    • 数组名

    • 函数名

scanf("%d",&num);   //&num 取址
  • 取址运算符得到是内存地址

指针变量
  • 需要保存内存地址,需要定义指针变量

指针变量定义
TYPE * pname;
​
TYPE *    是表示一个完整的类型,即指针类型
​
TYPE -  pname指针变量所存储了一个内存地址    TYPE表示内存地址中的数据为TYPE类型
*    -  说明是指针变量
TYPE *  说明地址(指针)的类型为  TYPE *
​
​
TYPE *p1,*p2;   //定义两个指针变量p1,p2
TYPE *px,py;    //px类型为TYPE*,  py类型为TYPE
​
​
在定义指针变量时  TYPE 至关重要,决定了解引用时   
    *p 能够取多少个字节的数据    sizeof(TYPE)

指针变量的初始化
int *p = NULL;             //可以   任意类型的指针变量都可以用NULL来初始
​
#define NULL  ((void *)0x0)
​
int *pn;                  //没有初始化    野指针    野指针不能解引用
int num = 0;
pn = &num;                //对指针变量进行赋值   pn存储了&num这个值(内存地址)
​
int *pm = &num;           //定义指针变量直接初始化
​

指针变量赋值 和 解引用 赋值
int a = 10,b = 20;
​
int *pa = &a;    //定义指针变量 存储&a    
//间接修改a的值   指针的意义  不直接通过变量名修改变量的值,而是通过内存地址
*pa = 1024;      //pa == &a    *pa  ==  *&a        *&a = 1024    a=1024
​
pa = &b;         //对指针变量pa进行赋值   pa存储&b    
*pa = 9527;      //pa指向了b     pa == &b     *&b = 9527    b=9527
​

解引用
*内存地址                  即获取内存地址中的数据
*指针变量                  指针变量的值就是内存地址
int num = 0x12345678;
char *pc = (char *)&num;
int *pi = &num; 
//pc 的值  和 pi的值 相等 
printf("%#X %#X\n",*pc,*pi);  //对pc和pi解引用  结果不一样
//指针变量pc和pi的类型不一样   char *   int * 
//TYPE * p = &x;    在对p进行解引用时 获取了 sizeof(TYPE)个字节的数据 
 
  • 指针变量必须要有明确的指向,才能解引用

取址&和解引用*是一对可逆的过程
&*a           ===   a
*&p           ===   p
​
&arr[0]   ==  &*(arr+0)   ==  &*arr ==  arr

空指针/野指针/万能指针
  • 空指针 NULL

  • 野指针 指针指向不确定 指针未初始化

    • 野指针不能直接解引用

  • void *

    • 内存地址就一编号 整数

    • 任意类型的指针都可以隐式转换为 void *

    • 指针的通用类型

    • void *类型的指针变量,不能解引用

指针算术运算
指针 + 1           即内存偏移   向后偏移1个单位的内存
TYPE *p = &x;
p + 1             偏移了 sizeof(TYPE) 字节大小的内存
​
指针 - 1           向前偏移1个单位的内存
++p;      //p = p + 1;   p保存偏移1个单位之后的内存
​
--p;      //p = p - 1;
*p++;     
两个指针可以相减(同类型)         得到偏移元素的个数

指针与[]闭合
ArrayName[index]    ==    *(ArrayName+index)    == *(index+ArrayName)
index[ArrayName]
  • 下标[]运算,本质上 指针偏移解引用

  • 数组名 本质 是 首元素的内存地址

  • *arr == arr[0]

  • *(arr+i) == arr[i]

二级指针
指针变量的内存地址
​
int num = 1024;
int *p = &num;    //p变量   p存储内存地址  同时它自己也有内存地址
​
int **pp = &p;    //p类型为int *     &p类型为   int **
pp    ==   &p
*pp   ==    *&p  ==  p  ==  &num
**pp  ==  *p   ==  *&num  == num
​
int x = 10;
*pp = &x;    //改变的是啥   改变了p的值 
**pp = 8527; //改变的是啥   改变了x的值
​
  • 对于任意类型T的变量,其内存地址(指针)类型为 T*

数组指针与指针数组
数组指针:  本质是指针,指针存储数组的内存地址(指针指向数组)
​
int arr[5] = {1,2,3,4,5};
int (*parr)[5] = &arr;    //数组指针  parr 和 &arr
​
int brr[3][7] = {};
int (*pbrr)[7] = brr;                     //数组名即首元素内存地址   &brr[0]   数组指针
​
int (*pcrr)[3][7] = &brr;   //对二维数组取地址   数组指针    二维数组指针
指针数组:  本质是数组,数组中元素的类型是指针
int a,b,c,d,e;
int *arr[5] = {&a,&b,&c,&d,&e};

函数指针 和 指针函数
函数指针:  本质是指针,即指针指向函数
​
void func(void){
​
}
​
void (*pf)(void) = func;   // &func;  *func;   pf就是函数指针
//pf指向的函数 返回值类型为void  且参数列表也为void
pf();
​
int bar(int a,int arr[],int n){}
​
int (*pb)(int,int *,int) = bar;
int arr[5] = {};
pb(10,arr,5);    //函数指针赋值之后  可以 当作函数名一样使用
//函数指针即存储函数的内存地址  当函数指针指向函数时,函数指针可以当作函数一样使用
指针函数:本质是函数,即函数返回指针类型
​
int *func(void){}
void *malloc(size_t size);


const与指针
const char *s1;      //const修饰 *s1  *s1只读    s1可以修改
     --常量指针       指针指向常量
char const *s2;      //const修饰 *s2
​
char * const s3;     //const修饰 s3  s3只读    *s3可以修改
    --指针常量   即指针是一个常量 
const char * const s4; //第一个修饰 *s4  第二个const修饰 s4

字符串

  • 一串连续的字符以'\0'作为结束标识

  • 串: 数据结构的概念 顺序结构 串/数组

  • C语言中没有单独为字符串创建一个独立的数据类型名

    java         string         stringbuffer   stringbuilder
    python       str
    c++          string

C语言中字符串
字面值字符串
  • 程序代码中凡是以"",是字面值字符串

  • 字面值字符串是常量(C语言中字面值都是常量),存储在代码段(字面值常量区)

  • 字面值字符串不能修改

  • 相同的字面值字符串在内存中只存储一份

  • 连续的字面值字符串 自动合并

    "Hello""world"  ==>  "Helloworld"
  • 用字符指针来记录字面值字符串

    • 字符指针只存储了字面值字符串的起始位置(内存地址)

    • 字符指针并没有把整个字符串的内存存储下来

字符数组来存储字符串
  • 字符数组存储字符串,一定要预留'\0'的内存

  • 字符数组 和 字符数组存储字符串

    • 字符数组而言,它可以末尾没有'\0'

    • 字符数组存储字符串,一定是要'\0'

  • 一般,字符数组存储整个字符串的内容

  • 普通局部的字符数组存储在栈内存

int main(){
    char s1[100] = "Hello world";   //用字面值字符串给字符数组初始化
    char s2[100] = {"Hello world"};
    
    char s3[] = "helloworld";       //s2数组多长  11  '\0'会被存储到字符数组中
    
    char s4[10] = "helloworld";     //数组越界
    
    return 0;
}

字符指针记录字符串起始位置
  • 字符指针指向字面值字符串

  • 字符指针指向字符数组

字符串操作函数
size_t strlen(const char *s);
char *strcpy(char *dest,const char *src);
char *strncpy(char *dest,const char *src,size_t n);
char *strcat(char *dest,const char *src);
char *strncat(char *dest,const char *src,size_t n);
int strcmp(const char *s1,const char *s2);
int strncmp(const char *s1,const char *s2,size_t n);
size_t strlen(const char *s){
    assert(s != NULL);
    size_t len = 0;
    for(;s[len]!='\0';++len);
    
    return len;
}
​
//把src字符串内存拷贝到dest中去  dest末尾一定保证'\0'
char *strcpy(char *dest,const char *src){
    assert(dest != NULL); //assert(dest != NULL && src != NULL);
    assert(src != NULL);
    char *pdt = dest;
    while((*dest++ = *src++)!='\0');  //先赋值 ,再用赋值之后的结果 和'\0'判断
    /*
    while((*dest = *src)!='\0'){      //while(*src != '\0') 最后需要手动'\0'
        ++dest;
        ++src;
    }
    */
    return pdt;
}
​
char *strncpy(char *dest,const char *src,size_t n){
    assert(dest != NULL && src != NULL);
    char *pdt = dest;
    while(n > 0 && (*dest = *src)!='\0'){
        --n;
        ++dest;
        ++src;
    }
    //src只有3个字符   n为10
    return pdt;
}
​
char * strncpy(char *dest, const char *src, size_t n){
    size_t i;
​
    for (i = 0; i < n && src[i] != '\0'; i++)
        dest[i] = src[i];
    for ( ; i < n; i++)  //是否有必要的问题
        dest[i] = '\0';
​
    return dest;
}
​
//追加 拼接到末尾
char *strcat(char *dest,const char *src){
    assert(dest!=NULL && src!=NULL);
    size_t len = 0;
    for(;dest[len]!='\0';++len);
    
    size_t i;
    for(i=0;src[i]!='\0';++i){
        dest[len+i] = src[i];
    }
    dest[len+i] = '\0';
    return dest;
}
​
char *strncat(char *dest,const char *src,size_t n){
    assert(dest != NULL && src != NULL);
    size_t len = 0;
    for(;dest[len]!='\0'; ++len);
    
    size_t i=0;
    for(;i<n && src[i]; ++i){
        dest[len+i] = src[i];
    }
    dest[len+i] = '\0';
    return dest;
}
​
//标准库 -1  0  1
int strcmp(const char *s1,const char *s2){
    assert(s1!=NULL && s2!=NULL);
    while(*s1 && *s2 && *s1 == *s2){
        ++s1;
        ++s2;
    }
    //return *s1 - *s2;   //负数  0  正数
    if(*s1 < *s2)
        return -1;
    if(*s2 < *s2)
        return 1;
    return 0;
}
​
int strncmp(const char *s1,const char *s2,size_t n){
    assert(s1!=NULL && s2!=NULL && n!=0);
    //先--n的原因是   最后一个字符不参与循环
    for(;--n>0 && *s1 && *s2 && *s1 == *s2;++s1,++s2);
    
    return *s1 - *s2;
}

#include <ctype.h>
​
int isalnum(int c);
int isalpha(int c);
int iscntrl(int c);
int isdigit(int c);
int isgraph(int c);
int islower(int c);
int isprint(int c);
int ispunct(int c);
int isspace(int c);
int isupper(int c);
int isxdigit(int c);
​
int isascii(int c);
int isblank(int c);


#include <string.h>
//根据delim进行拆分str
char *strtok(char *str, const char *delim);
//查找子串    KMP算法
char *strstr(const char *haystack, const char *needle);
char *strchr(const char *s, int c);
​
"192.168.102.33"         IP地址    对.进行拆分   192   168  102   33

动态内存

  • 动态内存 -- 堆内存

  • 堆 - 栈 内存

  • 堆 - 栈 数据结构

--linux 32位系统为例 进程独立4G内存空间    用户空间3G,内核空间1G
    sizeof(指针)  ==  4    [0x0000000,0XFFFFFFFF]
--linux 64位系统  进程256T内存空间       用户空间和内核空间各128T
    sizeof(指针)  ==  8byte         只用了48bit
​
4G内存空间[0X0000 0000 --  0XFFFF FFFF]
--------------------------------      >   0XFFFF FFFF
|                     |
|                     |
|   内核空间           |               程序代码无法直接访问
|                     |
|                     |
--------------------------------      >   0XBFFF FFFF     3G
| 环境列表 env         |
| main函数参数列表argv  |
--------------------------------
|                     |
|                     |              不初化垃圾值
|   栈内存             |              存储局部临时变量,函数调用时开销
|                     |              整体上:从大到小使用
|                     |
---------------------------------
|  加载的动态库         |
|  内存映射            |              堆栈缓冲区
|--------------------------------
|                     |
|                     |
|   堆内存             |              动态内存,手动申请,手动释放
|                     |              整体上:从小到大使用
|                     |
---------------------------------
|  BSS段              |  存储未初始化的全局变量及静态变量  bss自动清0(擦除)
|---------------------|        全局数据区   固定大小(当程序编译之后大小固定)
|  数据段              |  存储已经初始化全局变量和静态变量
----------------------------------
|  字面值常量区         |        只读     字符串字面值     编译之后内存固定大小
----------------------------------
|  代码区              |        只读     代码的机器指令   编译之后内存固定大小
----------------------------------    >  0X0408 0000
|  保留区              |
----------------------------------    >  0X0000 0000
​
#include <stdlib.h>
/*
* malloc向操作系统申请size个字节的堆内存
*    操作系统分配了>=size个字节  但是用户只能使用size个字节
*    (遵循申请多少个字节就使用多少个字节)
* 失败返回 NULL     成功返回  申请到内存的起始位置
* 第一次申请动态内存,至少分配33页动态内存  1页=4kb  = 4096byte
* 当free掉所有的动态内存时,最开始分配的33页依然保留   
* 除了前33页一次分配,后续不够时按页分配,释放也是以页为单位
*/
void *malloc(size_t size);
/*
*  calloc 向操作系统申请  nmemb * size 个字节的动态内存
*   失败返回 NULL     成功返回  申请到内存的起始位置
*  申请单个内存用malloc,申请多个元素的内存空间用calloc   但是没有绝对
*   malloc: size   <===>   calloc: nmemb*size
*   malloc对申请内存不会擦除(全部置0)
*   realloc对申请内存会全部擦除(0)
*/
void *calloc(size_t nmemb, size_t size);
/*
*  realloc 调整动态内存ptr内存块的大小   将内存块大小调整为size字节
*     失败返回 NULL     成功返回  调整之后动态内存的起始位置
*   第一种: 可以在原先内存块直接调整,返回的指针 等于 ptr
*   第二种:不能在原先内存块直接调整,额外重新申请size个字节内存空间,且将原ptr内存中的数据拷贝到 新内存块中,并且释放原ptr动态内存
*   ptr = realloc(ptr,size);   //用ptr重新去接收  管理返回之后的动态内存
*/
void *realloc(void *ptr, size_t size);
//  realloc:size == reallocarray:nmemb*size
void *reallocarray(void *ptr, size_t nmemb, size_t size);
​
//释放动态内存    同一块动态内存释放多次  段错误,double free
//ptr  一定是malloc/calloc/realloc/reallocarray函数的返回值
void free(void *ptr);
  • 动态内存,最开始申请时,是连续的内存空间,用户在申请时,将内存截取成内存块,但由于每次申请的内存大小又不一样,且需要索引到每一块动态内存,所以,每一块动态内存块,除了用户存储数据以外,还需要额外的一块 动态内存控制信息块(存储辅助信息: 当前内存块的字节大小,前后内存块的位置指针,是否空闲) 操作系统时 会详细讲解

  • 动态内存浪费内存空间 每申请一块动态内存,都会额外有一块内存用于存储 动态内存控制信息

  • 大块内存使用堆,小内存使用栈(栈不能胜任需求考虑堆)

  • 申请内存,不会一上来就直接给截取一块内存,它是会先从之前申请的动态内存块中查找,查找是否有合适的(内存块大小>=申请的字节大小)且处理空闲(之前申请过但被释放)的动态内存块,如果有,则直接将该内存标识为 已使用,然后返回给用户

#include <string.h>
void *memset(void *s, int c, size_t n);  //int c   只有一个byte有意义
//将内存s中n个字节,每个字节的数据设置成 c (虽然是int,但实际是char)
​
#include <strings.h>
//将s内存中n个字节全部置0
void bzero(void *s, size_t n);
​
#include <string.h>
//将src内存中n个字节拷贝到dest内存中       
void *memcpy(void *dest, const void *src, size_t n);//内存拷贝函数
void *memmove(void *dest, const void *src, size_t n);//功能和memcpy一样,但有安全检验      检验地址src和dest地址重叠问题
//内存比较函数  按字节逐一比较
int memcmp(const void *s1, const void *s2, size_t n);
​
void *memcpy(void *dest, const void *src, size_t n){
    char *cdt = (char *)dest;
    const char * csc = (const char *)src;
    int i;
    for(i=0;i<n;i++)
        *cdt = *csc;
    return dest;
}

int func(){
    static int a = 10; //只执行一次   存储在全局数据区
    --a;
    return a;
}
int ret = func(); //9
ret = func();     //8

编译原理

  • 编译四个步骤

    • 预处理 gcc -E xx.c

      • 删除注释

      • 导入头文件(预处理) #include

      • 宏替换 简单直接替换

      • 条件编译 #ifdef #if

      • 添加 #filename lineno 因为由于#include导致原本代码位置发生变化

      • 不会自动生成文件 需要加 -o

    • 编译 gcc -S xx.c/xx.i

      • 检查语法语义错误

      • 生成汇编代码

      • 自动生成 .s 汇编文件

    • 汇编 gcc -c xx.c/xx.i/xx.s

      • 将汇编代码汇编成机器指令 (根据指令对照表进行翻译)

      • 生成目标文件 .o

      • 目标文件不能运行

    • 链接 gcc *.o -o xxx

      • 将目标文件链接成可执行文件

      • 在编译阶段,对于函数调用,或者访问外部变量,只需要函数和变量的声明即可,在链接阶段,才会将函数调用以及访问外部变量进行绑定

预处理指令

  • 在C语言中以#开头,会在预处理阶段进行处理

  • 宏常量

     
    #define MACRO_NAME CONTEXT
    ​
    --MACRO_NAME  宏名称
    --CONTEXT     宏将被替换的内容    在预处理阶段,完整的简单替换
    宏函数

    #define MACRO_NAME(arg,..)   CONTEXT
    --带参数的宏         MACRO_NAME ()
    --宏函数的参数可以是类型
    • 关于简单替换 () 重要性

#define swap(a,b) \
   do{\
        typeof(a) __tmp = (a); \
        (a) = (b); \
        (b) = __tmp; \
   }while(0)
//TYPE 类型名   结构体类型名    
//MEMBER  TYPE结构体类型中的一个成员名
//经过对齐补齐之后,MEMBER结构体成员距离基准位置(起始)偏移的字节数
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
​
//ptr  结构体成员指针(type类型结构体变量中member成员的位置)
//type  结构体类型名
//member 结构体成员名
//获得结构体变量的起始位置
#define container_of(ptr, type, member) ({                 \
   const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
   (type *)( (char *)__mptr - offsetof(type,member) );})
​
#define LOG(format,__va_args__...) printf("%s %s %d"format,__FILE__,__func__,__LINE__,##__va_args__)
  • #undef

  • #可以把任何标识变成字符串

  • ##连接作用

#include
  • 包含头文件

  • 在预处理时,将头文件的内容预处理之后导入到当前文件

    #include <>        //从系统指定的目录下查找头文件
    #include ""        //先从当前代码目录查找头文件,如果没有,则从系统指定的目录查找 
    • 标准库,操作系统头文件建议使用<>

    • 自己的头文件建议使用 ""

  • 关于查找头文件,在编译时,也可以指定 gcc xx.c -I 指定查找头文件的路径

条件编译
  • 在预处理阶段,进行处理,只会保留预处理条件为真的部分代码 (选择性编译代码)

  • if/switch 语句代码分支都会编译到可执行文件中,运行时选择对应的分支执行

  • #if/#ifdef/#ifndef 条件编译,只会保留预处理条件为真的代码

    • 运行效率高(运行时不需要经过判断)

    • 可执行文件较小

#ifdef  MACOR
​
#endif
#ifndef MACOR
​
#endif
#ifdef MACOR
​
#else
​
#endif
#ifndef MACOR
​
#else
​
#endif
  • 头文件卫士

    • 防止头文件被重复包含

      #ifndef HEADER_FILE_NAME_H__
      #define HEADER_FILE_NAME_H__
      ​
      //头文件的内容
      ​
      #endif //HEADER_FILE_NAME_H__

#if  预处理条件                  //预处理阶段就能计算出结果
​
#endif
#if 预处理条件
​
#else
​
#endif
#if cond1
​
#elif cond2
​
#elif cond3
​
...
#endif
#if cond1
​
#elif cond2
​
#elif cond3
​
...
#else
    
#endif

other
#line  no  指定行号
#error 错误
#warning 警告
#pragma GCC posion goto
#pragma GCC pack(1/2/4/8)

typedef 和 #define
typedef int *  TPTR;
#define DPTR int *
​
TPTR  a,b;
DPTR  m,n;
  • typedef

    • 给类型取别名

      typedef unsigned long int  size_t;
      ​
      typedef int ARR[5];   //ARR
      ​
      typedef int (*HANDLE)(int,void *);  //HANDLE
      ​
      typedef struct Stu Stu;
      ​
      typedef struct Stu{
          //...
      }Stu;

结构体/联合/枚举

  • 结构体复合数据类型

  • 看作不同类型变量的集合

定义结构体类型
struct 结构体名
{
    type member;  //成员变量
    //...
};
  • 定义结构体类型时,并不会给成员变量分配内存空间

定义结构体变量
struct STRUCT_NAME var_name;   
​
结构体变量初始化
  • {} 初始化器,只有在初始化时才能使用{},后续不能使用{}赋值

    struct  STRUCT_NAME var_name = {v1,v2,...};  //跟成员的定义顺序一致
    {.member = v1,....}

结构体成员访问
  • .运算符

  • 结构体变量名.成员名

结构体指针 和 ->
  • 结构体指针变量访问成员可以直接使用->

  • (*pstu).no pstu->no

柔性数组
struct A{
    int n;
    int arr[];   // int arr[0]       柔性数组 
};

结构体数组 和 动态内存存储结构体数据

  • 练习

    定义一个员工类型   
        员工号,员工姓名,联系方式,工资
    struct Emp{
        int no;
        char name[64];
        char phone[24];
        float salary;
    };
    能够独立的完成这些功能:
        实现增加员工,删除员工,查找员工(员工号,员工姓名),根据工资从高到低显示,根据员工号顺序显示

  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值