C语言

在Linux操作系统下C语言程序开发的步骤

1.使用vi编写C语言源文件
2.使用命令gcc <源文件名>得到最终的可执行文件
3.使用命令./a.out执行得到的可执行文件

#include <stdio.h> /*导入stdio.h文件(在/usr/include目录下)*/
int main(){ /*int为main函数的返回类型*/
    printf("hello world!\n");
    return 0;
}

说明:C语言中以#开头的指令叫做预处理指令,#include用于把一个文件的内容加入到当前源文件中
        <>包括的文件名不会查找当前目录,用""包括的文件名会查找当前目录.只能使用#include包含.h文件而不应该包含.c文件

gcc编译工具的介绍

gcc:GNU(开放源代码组织) Copmiler(编译器) Collection(一套)

gcc所以支持后缀名解释

  • .c:     C原始程序
  • .C/.cc/.cxx     C++原始程序
  • .m              Objective-C原始程序
  • .i                 已经过预处理的C原始程序
  • .ii                       已经过预处理的C++原始程序
  • .s/.S                  汇编语言原始程序
  • .h                       预处理文件(头文件)
  • .o                       目标文件
  • .a/.so                编译后的库文件  

gcc常用选项
 -v:显示各种相关信息
 -E:只完成预处理指令的工作(#开头的代码)
 -std=c89/-std=c99:指定采用的C语言标准版本(用c89标准制作出来的的一定适用于c99,反之不行)
 -S:转化成汇编语言源程序,hello.c ->hello.s
 -O:优化(1,2,3,三种不同的优化级别)
 -c:只对源程序进行编译工作,不链接(不生成可执行文件就是-o后面的文件,默认是a.out),通常用于编译不包含主程序的子程序文件
 -o:用于指定结果执行文件的名字(默认为a.out)
 -Wall:把所有的警告都打印出来

-g:产生符号调试工具(GUN的gdb)所必要的符号资讯,要想对源代码进行调试,就必须加入这个选项

-I:dirname,将dirname所指出的目录添加到程序头文件列表中,是在预编译过程中使用的参数

-L:dirname,将dirname所指出的目录添加到程序函数档案库文件的目录列表中,是在连接过程中使用的参数

编译器的主要组件

  1. 分析器:分析器将源代码转为汇编语言,因为要从一种格式转换为另外一种格式(C到汇编),所以分析器需要知道目标机器的汇编语言
  2. 汇编器:将汇编语言代码转换为CPU可以执行字节码
  3. 链接器:将汇编器生成的目标文件组合成可执行的应用程序。链接器需要知道这种目标格式以便工作
  4. 标准C库:核心的C函数都有一个主要的C库提供。如果在应用程序中用到了C库中的函数,这个库就会通过链接器和源代码连接来生成最终的可执行程序

 

C语言程序的处理过程:

  1. 预处理,完成预处理指令要求的工作 ,gcc -E test.c -o test.i
  2. 编译,把C语言源程序翻译成计算机认识的格式,gcc -S test.i -o test.s
  3. 汇编,gcc -c test.s -o test.o 或者 as test.s -o test.o, 使用file test.o命令查看:

    test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

  4. 链接,把程序中各个部分链接起来,gcc test.o -o test -lm 
#include <stdio.h>
#include <math.h>

#define N 10
#define _DEBUG_

int main(int argc, const char *argv[]){
    printf("argc=%d\n", argc); //命令行参数
    printf("argv=%s\n", argv[0]); //第一个参数为文件名
    double m = 615, n;
    
    m += N;

    n = sqrt(m);
#ifdef _DEBUG_
    printf("debug:m=%lf n=%lf\n", m, n);
#else
    printf("release:m=%lf n=%lf\n", m, n);
#endif
    return 0;
}

Gdb调试流程

使用-g选项:gcc -g test.c -lm ; gdb a.out,进行代码调试

调试的一些命令:

  • l:查看文件
  • b:设置断点,b 6表示在第6行设置断点
  • del:删除断点,del 1表示删除第一个断点
  • info b:查看断点情况
  • r:运行代码
  • p 变量名:查看变量值
  • n:单步运行
  • s:单步运行,进入函数内部
  • c:恢复程序运行,执行运行到下一个断点

注释

注释用来在程序中加入文字信息,分单行注释(//注释内容)和多行注释(/*注释内容*/)>    两种.注释中的内容会被计算机忽略

 变量

变量用来记录数字,在使用变量之前必须声明,声明语法如下:
  变量类型 变量名,比如:int price
在未赋值之前变量内部有数据,但是具体数字不确定.使用=(赋值操作符)对变量进行赋值,第一次给变量赋值叫做变量初始化.可以在一行语句中声明多个同类型的变量

变量与内存的关系:计算机内存由大量的字节构成,内存中的数字必须记录在一个或多个相邻的字节中.变量是这些字节的代表

通过&符号可以获得变量的字节地址,通过*符号获得字节地址对应的变量

#include <stdio.h> 
int main(){ 
    int num = 0;
    printf("&num是%p\n", &num);
    *(&num) = 10;
    printf("num是%d\n", num);
    return 0;
}

现在有两个变量,实现两个变量之间的交换

#include <stdio.h>
int main(){
    int num1 = 7, num2 = 3, temp = 0;
    //使用临时变量完成两个变量内容的交换
    printf("num1:%d\nnum2:%d\n", num1, num2);
    /*temp = num1;
    num1 = num2;
    num2 = temp;*/

    //不使用临时变量完成两个变量内容的交换
    num1 = num1 + num2;
    num2 = num1 - num2;
    num1 = num1 - num2;
    printf("交换后num1:%d\nnum2:%d\n",num1,num2);
    return 0;
}

全局变量,局部变量和块变量

  定义在函数之外的变量叫做全局变量,
  定义在函数内部的变量叫做局部变量,
  定义在函数内部的大括号里边的变量叫做块变量
  生存周期表示一个变量在什么时间范围内是可以使用的.
  作用域表示一个变量在什么代码范围内是可以使用的.
  全局变量的生存周期是整个程序的运行时期,局部变量的生存周期是所在函数的执行过程,块变量的生存周期是所在块的执行过程.
  全局变量的作用域是整个程序代码范围,局部变量的作用域是所在函数的执行体代码,块变量的作用域是所在块的代码.
  不同层次的变量可以重名,在使用的时候变量名称匹配的是所有作用域包括使用语句的变量中层次最深,并且在这条语句之前声明的那个变量
  没有被初始化的全局变量会被自动初始化成0.

#include <stdio.h>
int shu_zi = 0;
int main(){
    printf("shu_zi:%d\n", shu_zi);//0
    int shu_zi = 1;
    //int shu_zi = 2; // 错误,重复定义
    {
        {
      int shu_zi = 2;
      printf("shu_zi:%d\n", shu_zi);//2
      }
    printf("shu_zi:%d\n", shu_zi);//1
    }
    printf("shu_zi:%d\n", shu_zi);//1
    return 0;
}

使用全局变量实现一个栈

#include <stdio.h>
#include <stdbool.h> //用于导入bool类型的数据
int last = -1; //表示栈中最后一个数据所占的位置
//定义一个全局数组用来表示一个大小为20的栈
int stack[20];
//判断栈是否为空
bool empty(){
    if(last < 0){
        return true;
    }
    else{
        return false;
    }
}
//判断栈是否满了
bool full(){
    if(last == 19){
        return true;
    }
    else{
        return false;
    }
}
//向栈中添加数据,当栈满的时候添加失败
bool push(int num){
    if(!full()){
        stack[++last] = num;
    return true;
    }
    else{
        return false;
    }
}
//取出最后一个数据
bool pop(){
    if(!empty()){
        last--;
    return true;
    }
    else{
        return false;
    }
}
//获取最后一个数据,并不取出
int peak(){
    return stack[last];
}
//使用四则运算2+3*5表达式,验证结果是否正确
int main(){
    int num1 = 0, num2 = 0, operator=0;
    push(2);
    push('+');
    push(3);
    push('*');
    push(5);
    num2 = peak();
    pop();
    operator = peak();
    pop();
    num1 = peak();
    pop();
    num2 *= num1;
    operator = peak();
    pop();
    num1 = peak();
    pop();
    num2 += num1;
    printf("计算结果是%d\n", num2);
    if(empty()){
        printf("栈正常\n");
    }
    return 0;
}

标识符命名规则

1.必须以字母或下划线开头
2.后面的可以是字母,数字或下划线
3.大小写不同的字母被认为不同
4.不能和C语言关键字冲突
5.长度没有限制,但是只有前面的是有效的(c89取前31位,c99取前63位)

C语言的代码规范

1.一条语句可以放在多行内,但是多条语句不应该放在一行里
2.在合适的地方添加空格,这有助于提高程序可读性
3.不同层次的代码要用缩进区分开
4.适当使用空行和注释区分不同的逻辑单元
5.标识符名称采用驼峰方式或下划线方式

printf与scanf函数

使用C语言提供的printf函数实现把信息打印在屏幕上的功能,需要包含一个stdio.h文件.第一个参数是一个字符串,用来表示要打印的信息.
格式:
  %d:用不带小数点的数字替换,%md表示数字占用的宽度
  %c:用单个字符替换,单引号表示字符
  %s:用一个字符串替换
  %f:用一个带小数点的数字替换
  %g:也可以用一个带小数点的数字替换,输出是后面没有多余的0
  %p:用一个地址数据替换

#include <stdio.h>
int main(){
    printf("Hello World!\n");//\n表示换行
    printf("Hello \rWorld\n");//\r表示从行首开始,World
    printf("Hel\\lo World!\n");//\\表示单个斜杠
    printf("Hello Wor\"ld\n");
    int num;
    printf("整数是%d\n", num);//num表示随机的整数
    printf("整数是%5d\n", 1);//表示整数占用的宽度
    printf("字符是%c\n", 'a');//单引号表示字符
    printf("字符串是%s\n", "abc");
    printf("数字是%f\n", 3.5f);//3.500000
    printf("数字是%0.2f\n", 3.5f);//保留2位小数
    printf("数字是%lf\n", 3.5);
    printf("数字是%g\n", 3.5f);//3.5
    printf("地址是%p\n", &num);//0x7ffd9e933af4
    return 0; 
}

scanf函数用于从屏幕上读取信息并记录到变量中,格式字符串与printf函数类似,后面的参数必须是变量的地址,否则会导致段错误.格式串中的类型必须和后面给定地址的变量类型一致

练习:让用户从屏幕上输入圆的半径,然后打印圆的面积和周长

#include <stdio.h>
#include <math.h>
int main(){
    float r = 0.0f;
    printf("请输入圆的半径:");
    scanf("%f", &r);
    printf("圆的周长是%g\n", 2 * 3.14 * r);
    printf("圆的面积是%g\n", 3.14 * r * r);
    return 0;
}

 数据类型

ASCII码表记录字符和编码之间对应关系
char数据类型来表示字符编号数据,占一个字节,表示范围-128到127
unsiged char类型也占有一个字节,表示范围从0到255

short类型占有两个字节,使用%hd匹配,范围在(-32768~32767)
unsigned short类型占有两个字节,使用%hu匹配,范围在(0~66535)
long类型占有4个字节使用%ld匹配,范围在(-2**31~2**32-1)
unsigned long类型占有4个字节,使用%lu匹配,范围在(0~2**32-1)
int类型的大小取决于计算机的单次处理能力,在使用32位计算机上与long相同.
float(浮点类型)用来表示带小数点的数字,由于精度原因计算中要考虑误差,占有4个字节使用%f或%g(省略后面的0)匹配,没有取值范围
double(双精度浮点数)用来表示更高精度的浮点数,也存在误差问题.占有8个字节使用%lf匹配,没有取值范围

有符号数字的二进制表示形式中最高位是0则表示这个数一定是非负数,如果是1则一定是负数,我们把这个最高位的二进制为称为符号位

在printf和scanf函数中使用%hhd与char类型变量匹配,使用%hhu与unsigned char类型变量匹配
sizeof关键字可以用来计算某个数据类型或某个变量所占空间的大小,以字节为单位.它不会计算小括号中的表达式.

#include <stdio.h>
#include <limits.h>
int main(){
    char aChar = 'a';
    unsigned char uChar = 0;
    printf("aChar是%c,aChar的编号是%d\n", aChar, aChar);
    aChar = 98;
    printf("aChar是%c,aChar的编号是%d\n", aChar, aChar);
    printf("sizeof(char)是%d字节,sizeof(aChar)是%d字节\n",sizeof(char),sizeof(aChar));
    printf("sizeof(aChar=99)是%d字节\n", sizeof(aChar=99));
    printf("aChar是%d\n", aChar);
    printf("最大字符编码是%d,最小字符编号%d\n", CHAR_MAX, CHAR_MIN);
    printf("sizeof(unsigned char)是%d字节,size(uchar)是%d字节\n", sizeof(unsigned char),sizeof(uChar));
    printf("最大字符编码是%d\n", UCHAR_MAX);
    aChar = 191;
    uChar = 191;
    printf("aCHar:%hhd\n", aChar);
    printf("uCHar:%hhu\n", uChar);
    return 0;
}
/*aChar是a,aChar的编号是97
aChar是b,aChar的编号是98
sizeof(char)是1字节,sizeof(aChar)是1字节
sizeof(aChar=99)是1字节
aChar是98
最大字符编码是127,最小字符编号-128
sizeof(unsigned char)是1字节,size(uchar)是1字节
最大字符编码是255
aCHar:-65
uCHar:191*/

小写字母转大写

#include <stdio.h>
int main(){
    char letter;
    printf("请输入一个小写字母:");
    scanf("%c", &letter);
    printf("%c对应的大写字母为%c\n",letter,letter - 'a' + 'A');
    return 0;
}

运算符

+, -, *, /分别表示加减乘除.
%表示取余操作,不能对浮点数进行取余操作

#include <stdio.h>
int main(){
    unsigned short num = 0;
    printf("请输入一个两位数:");
    scanf("%hu", &num);
    //%运算符取余数,/运算符取模
    printf("结果是:%d%d\n", num % 10, num / 10);
    return 0;
}

=表示赋值操作符,要求左边的部分一定要是一个左值.赋值操作符从右向左计算.
符合操作运算符指的是在赋值操作符前面加上其他的操作符并形成的,例如+=, *=
++表示自增运算,--表示自减运算,只能对左值进行计算
逻辑运算符计算结果只有两个,一个真(对)用1表示,另外一个是假(错)用0表示.我们也可以在代码中使用0表示错误,任何其他数值表示真(对).
==(相等), !=(不相等), >(大于), <(小于), >=(大于等于), <=(小于等于), !(求反) 

#include <stdio.h>
int num = 10;
int main(){
    int num1 = 0, num2 = 0;
    num1 = 8, 3;//8
    printf("num1:%d\n", num1);//逗号也是运算符优先级比=还低
    num2 = (8, 3);//3
    printf("num2:%d\n", num2);//逗号操作符返回最后一个表达式的计算结果
    num2 *= 2 + 3;// num2 = num2 * (2 + 3) = 15
    printf("num2:%d\n", num2);
    num2 ++;//num2 = num2 +1
    printf("num2:%d\n", num2);//16
    ++ num2;
    printf("num2:%d\n", num2);//17
    num1 = num2++;//先将num2赋值给num1然后在自加,17
    printf("num1:%d\n", num1);
    num1 = ++num2;//先将num2自加再赋值给num1,19
    printf("num1:%d\n", num1);
    num2 = num1++ + ++num1;
    printf("num1:%d\n",num1);//21
    printf("num2:%d\n",num2);//40
    num2 = num++ + ++num;
    printf("num2:%d\n", num2);//22,不同的编译器结果可能不同21
    printf("num:%d\n", num);//12
    return 0;
}

编写一个时钟程序

#include <stdio.h>
#include <time.h>
#include <unistd.h>//导入sleep函数
int main(){
    unsigned long total_seconds = 0;
    int hours = 0, minutes = 0, seconds = 0;
    while(1){
    total_seconds = time(0);
    seconds = total_seconds % 60;
    minutes= total_seconds % 3600 / 60;
    hours = total_seconds % (24 * 3600) / 3600;
    hours = (hours + 8) % 24;
        printf("%02d:%02d:%02d\r", hours, minutes, seconds);
    fflush(stdout);//强制把输出缓冲区清空到屏幕上
    //while(time(0) == total_seconds);//一直占用cpu影响计算机性能
        sleep(1);
    }
    return 0;
}

 数组

概念:程序中需要使用变量记录数字,如果数字的数量很多就需要使用多个变量.可以把多个同类型的变量合成一个整体,C语言里使用数组代表这个合并后的整体

数组需要先声明然后才能使用

数组一旦存在则面包含的变量个数就不可以改变

使用下标表示数组中每个变量

可以使用for循环依次处理数组中每个变量

数组也应该先初始化然后在使用

#include <stdio.h>
#include <stdlib.h>
int main(){ 
    int arr[5] = {};//数组声明语句,并初始化为5个0
    //如果初始话的数组的长度小于5则用0补,不如大于5则截取
    arr[2] = 10;
    printf("arr[2]是%d\n", arr[2]);
    int num = 0;
    for(num = 0; num <=4; num++){
        arr[num] = num + 1;
    }
    for(num = 0;num <= 4;num++){
        printf("%d ", arr[num]);
    }
    printf("\n");
    return 0;
}

示例:编写程序产生一张彩票,彩票中包含7个1到36之间的随机数(不能重复)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){ 
    int arr[7] = {}, cnt = 0, num = 0;
    srand(time(0)); 
    do {
        //将数组的第一个位置赋值
        arr[cnt] = rand() % 36 + 1;
    //将后面的数字分别与前面的比较直到与前面的全不相等为止
        for(num=0; num <= cnt - 1; num++){
        if(arr[cnt] == arr[num]){
            break;
        }
    }
    //判断上述循环是否为正常结束
        if(num == cnt){
            cnt++;
        }
    }while(cnt < 7);
    for (num = 0; num <= 6; num++){
        printf("%02d ",arr[num]);
    }
    printf("\n");
    return 0;
}

数组的优缺点:

  优点:数组支持随机访问,数组中每一个变量都可以直接使用,不需要依赖其他变量

  缺点:数组不适合进行插入删除操作

编写程序向一个有序数组中插入一个新数字,插入后整个数组仍然是有序的,然后再删除一个数字

 

#include <stdio.h>
int main(){ 
    int arr[9] = {1, 3, 5, 7, 9, 11}, num = 0;
    for (num = 5; num >= 0; num--){
        if(arr[num] > 4){
        arr[num + 1] = arr[num];
    }
    else{
        break;
    }
    }
    arr[num + 1] = 4;
    for(num = 0; num <= 6; num++){
        if(arr[num] > 7){
        arr[num -1] = arr[num];
    }
    }
    for (num = 0; num < 6; num++){
        printf("%2d ",arr[num]);
    }
    printf("\n");
    return 0;
}

 流程控制语句

分支语句:if语句和switch语句

if语句格式:
if(条件1){//如果 每个分之语句if只能出现1次
     //满足条件1时执行的语句
 }else if(条件2){//或者 出现0~n次
     //满足条件2时执行的语句                                                                                                                         
 }else{//否则 出现0~1次
     //以上条件否不满足是执行的语句
 }
switch语句格式:

switch(控制表达式){
     case 常量表达式:语句;
     ...
     default:语句;
 }
 控制表达式被当做整数处理,可以是字符,但不能是浮点数
 常量表达式必须是常量,如3 'A' 2+5
 语句可以是0到多条                                                                                                                                   
 不允许重复重复分支,default不一定在最后
 break用于退出switch,switch语句遇到break后或执行完最后一条语句才退出

#include <stdio.h>
int main(){ 
    int score = 0;
    printf("请输入1~100的整数:");
    scanf("%d", &score);
    switch(score / 10){
        case 10:
    case 9:
        printf("A\n");
        break;
    case 8:
        printf("B\n");
        break;
        case 7:
        printf("C\n");
        break;
    case 6:
        printf("D\n");
        break;
    default:
        printf("E\n");
    }
    return 0;
}

循环语句
  for循环格式:
  for(表达式1;表达式2;表达式3){
      循环体语句;
  }
  执行顺序:表达式1 -> 表达式2(为假则退出循环) -> 循环体语句(遇到break可则退出循环) -> 表达式3
  示例:打印99乘法表

#include <stdio.h>
int main(){ 
    int i = 1, j = 1;
    for(i = 1; i < 10; i++){
        for(j = 1; j <= i; j++){
        printf("%d*%d=%2d ", j, i, i*j);
    }
    printf("\n");
    }
    return 0;
}

while循环格式
  while(表达式){
     语句;
  }
 do ... while循环格式                                                                                                                                                 
  do{
      语句;
  }while(表达式);
循环内部常用的关键字

  break用于退出循环,可以到达循环的外部下一行语句

  continue用于终止本次循环,继续下一次循环

指针

C程序设计中使用指针可以使程序简介、紧凑、高效;有效地表示复杂的数据结构;动态分配内存;得到多于一个的函数返回值

地址和变量:

  在计算机内存中,每一个字节单元,都有一个编号,称为地址

在C语言中,内存单元的地址称为指针,专门用来存放地址变量,称为指针变量,在不影响理解的情况中,有时对地址、指针和指针变量不区分,统称指针

指针变量的声明:

  一般形式如下:

    <存储类型>  <数据类型> * <指针变量名>  

    说明: 存储类型默认是auto 可以省略

    例如:char *pName

  指针的存储类型是指针变量本身的存储类型

  指针声明时指定的数据类型不是指针变量本身的数据类型,而是指针目标的数据类型,简称为指针的数据类型。

 指针在声明的同时,也可以被赋予初值,称为指针的初始化

  一般形式如下:

    <存储类型>  <数据类型> * <指针变量名>  =  <地址量>

    例如:int a, *pa=&a //把变量a的地址作为初值赋予了刚声明的int型指针pa

    int a =3 //int a; a=3

    int *pa = &a; // int * pa; pa=&a

    说明:

      pa表示指针变量,它的内容是地址变量

      &pa表示指针变量所占用的内存区域的地址,是个常量

      *pa表示指针所指向的对象,它的内存是数据

指针指向的内存区域中的数据称为指针的目标

如果它指向的区域是程序中的一个变量的内存空间,则这个变量称为指针的目标变量,简称为指针的目标

把一个已有地址值的指针变量赋给具有相同数据类型的另外一个指针变量,例如

  float a, * px, *py;

  px = &a;

  py = px;

把一个数组的地址赋给具有相同数据类型的指针

  int a[20], *pa;

  pa = a; //等价于 pa = &a[0]

 指针的运算

  指针运算是以指针变量所存放的地址量作为运算量而进行的运算

  指针运算的实质就是地址的计算

  指针运算的种类是有限的,它只能进行赋值运算、算数运算和关系运算

注意:

  不同数据类型的两个指针实行加减整数运算是无意义的

  px + n 表示的实际位置的地址量是:px + sizeof(px的类型) * n

  px - n 表示的实际位置的地址量是:px - sizeof(px的类型) * n

两指针相减运算

  px - py 运算的结果是两指针指向的地址位置之间相隔数据的个数,因此两指针相减不是两指针持有的地址值相减的结果

  两指针相减的结果值不是地址量,而是一个整数值,表示两指针之间相隔数据的个数

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[5] = {3, 5, 6, 1, 4} ;
    int *p, *q;
    
    p = &a[0];
    q = &a[3];
    
    printf("%p %p\n", p, q); // 0x7ffeb9563680 0x7ffeb956368c
    printf("%d %d\n", *p, *q);// 3 1
    printf("%ld\n", q-p); // 3  12/4=3
    return 0;
}

指针加一,减一运算

#include <stdio.h>

int main(int argc, char *argv[])
{
    double a[5] = {3, 5, 6, 1, 4} ;
    double *p, *q, *r;

    p = a; // p = &a[0]
    printf("%p %f\n", p, *p); // 0x7ffe5e5b7cc0 3.000000
    q = p++;
    printf("%p %f\n", p, *p); // 0x7ffe5e5b7cc8 5.000000
    printf("%p %f\n", q, *q); // 0x7ffe5e5b7cc0 3.000000
    r = ++p;
    printf("%p %f\n", p, *p); // 0x7ffe5e5b7cd0 6.000000
    printf("%p %f\n", r, *r); // 0x7ffe5e5b7cd0 6.000000
    return 0;
}

 指针的关系运算

  两指针之间的关系运算表示他们指向的地址位置之间的关系。指向地址大的指针大于指向地址小的指针

  指针与一般整数变量之间的关系运算没有意义但可以和零进行等于或不等于的关系运算,判断指针是否为空

#include <stdio.h>
#define N 5
int main(int argc, char *argv[])
{
    int a[N];
    int *p, i;
    p = a;
    for(i = 0; i < N; i++){
        scanf("%d", p++);
    }
    p = a;
    for(i = 0; i < N; i++){
        printf("%d ", *p++);
    }
    puts("");
    return 0;
}

指针与数组

  在C语言中,数组的指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的起始地址

  一维数组的数组名为一维数组的指针(起始地址),例如

  double x[8]; 因此x为x数组的起始地址

  设指针变量px的地址等于数组指针x(即指针变量px指向数组的首元素)则:

    x[i] 、*(px+i)、*(x+i)和px[i]具有完全相同的功能:访问数组第i+1个数组元素

#include <stdio.h>
int main(int argc, char *argv[])
{
    int a[] = {5, 6, 4, 2};
    int *p, i, n;
    
    p = a;
    n = sizeof(a) / sizeof(int);
    for(i = 0; i < n; i++){
        printf("%d %d %d %d;", a[i], *(p+i), *(a+i), p[i]); // 5 5 5 5;6 6 6 6;4 4 4 4;2 2 2 2;
    }
    puts("");
    return 0;
}

 使用指针将一个整形数组中n个数按反序存放

#include <stdio.h>
int main(int argc, char *argv[])
{
    int a[] = {1, 6, 9, 12, 32, 43, 5};
    int *p, *q, n, i, t;

    n = sizeof(a) / sizeof(int);
    p = a;
    q = &a[n-1];
    for (i = 0; i < n; i++){
        printf("%d ", a[i]);
    }
    puts("");
    while (p < q){
        t = *p;
        * p = *q;
        *q = t;
        p++;
        q--;
    }
    for (i = 0; i < n; i++){
        printf("%d ", a[i]);
    }
    puts("");
    return 0;
}

 指针与二维数组

  多维数组就是具有两个或两个以上下标的数组,在C语言中,二维数组的元素连续存储,按行优先存

  二维数组名代表数组的起始起始地址,数组名加1,是移动一行元素。因此。二维数组名常被称为行地址

  行指针(数组指针)

  存储行地址的指针变量,叫做行指针变量。形式如下:

    <存储类型> <数据类型> (*<指针变量名>)[表达式]

    例如:int a[2][3]; int (*p)[3];

  方括号中的常量表达式表示指针加1,移动几个数据。

  当用行指针操作二维数组时,表达式一般写成1行的元素个数,即列数

字符指针与字符串

  C语言通常使用字符数组来处理字符串

  通常,我们把char数据类型的指针变量称为字符串指针变量。字符指针变量与字符数组有着密切关系,它也被用来处理字符串

  初始化字符指针是把内存中字符串的首地址赋予指针,并不是把该字符串赋值到指针中

    char str[] = "Hello World";

    char *p = str;

  在C语言编程中,当一个字符指针指向一个字符串常量时,不能修改指针指向的对象的值

    char * p = "Hello world";

    *p = 'h'; // 错误,字符串常量不能修改

指针数组

  所谓指针数组是指由若干个具有相同存储类型和数据类型的指针变量构成的集合

  指针数组的一般声明形式:

    <存储类型> <数据类型> *<指针数组名>[<大小>]

    指针数组名表示该指针数组的起始地址

  声明一个指针数组:

    double * pa[2], a[2][3];

  把一维数组a[0],a[1]的首地址分别赋予指针变量数组的数组元素pa[0]和pa[1]

    pa[0] = a[0]; // 等价于pa[0] = &a[0][0];

    pa[1] = a[1]; //等价于pa[1] = &a[1][0];

  此时pa[0]指向了一维数组a[0]的第一个元素a[0][0],而pa[1]指向了一维数组a[1]的第一个元素a[1][0]

#include <stdio.h>
int main(int argc, char *argv[])
{
    double * pa[2];
    double a[2][3] = {{1,4,6},{12,9,7}};
    pa[0] = a[0];
    pa[1] = a[1];
    
    printf("%f\n", a[0][1]);
    printf("%f\n", *(a[0]+1));
    printf("%f\n", *(pa[0]+1));
    return 0;
}

多级指针

  把一个指向指针变量的指针变量,称为多级指针变量

  对于指向处理数据的指针变量称为一级指针变量,简称一级指针

  而把执行一级指针变量的指针变量称为二级指针变量,简称二级指针

  二级指针变量的声明形式如下:

    <存储类型> <数据类型> ** <指针名>;

#include <stdio.h>
int main(int argc, char *argv[])
{
    char * s[] = {"apple", "pear", "potato"};
    char ** p;
    int i, n;
    i = 0;
    n = sizeof(s) / sizeof(char *);
    p = &s[0]; // p = s
    while (i < n){
        printf("%s %s\n", s[i], *(p +i));
        i++;
    }
    return 0;
}

void指针和const修饰符

  void指针

    void指针是一种不确定数据类型的指针变量,它可以通过强制类型转换让该变量指向任何数据类型的变量

    一般形式:
      void * <指针变量名称>;

    对于void指针,在没有强制类型转换之前,不能进行任何指针的算数运算

#include <stdio.h>
int main(int argc, char *argv[])
{
    int m = 10;
    double n = 3.14;
    void * p, * q;
    p = &m;
    q = &n;
    printf("%d %d\n", m, *(int *)p);
    printf("%.2f %.2f\n", n, *(double *)q);
    return 0;
}

  const变量

    常量化变量值

    一般声明形式如下:

      const <数据类型> 变量名 = [<表达式>];

    常量化变量是为了使得变量的值不能修改

    变量有const 修饰时,若想用指针间接访问变量,指针也要有const修饰,const放在指针声明的什么位置呢?

  const修饰指针

    常量化指针目标表达式一般声明形式如下:

      const <数据类型> * <指针变量名> [ =  <指针运算表达式>]

    常量化指针目标是限制通过指针改变其目标的数值,但是<指针变量>存储的地址值可以修改

#include <stdio.h>
int main(int argc, char *argv[])
{
    int m = 10;
    const int * p;
    p = &m;
    (*p)++;//通过指针该目标,error: increment of read-only location ‘*p’
    return 0;
}

    常量化指针目标表达式一般声明形式如下:

      <数据类型> * const <指针变量名> [ =  <指针运算表达式>]

    使得<指针变量>存储的地址值不能修改,但可以通过*<指针变量名称>可以修改指针所指向变量的数值

#include <stdio.h>
int main(int argc, char *argv[])
{
    int m = 10;
    int * const p = &m;
    (*p)++;//通过指针该目标
    printf("%d\n", m);
    return 0;
}

 函数

函数是一个完成特定功能的代码模块,其程序代码独立,通常要求有返回值,也可以是空值

一般形式如下:

  <数据类型> <函数名称> (<形式参数说明>){

    语句序列;

    return [(<表达式>)];

  }

  <数据类型>是整个函数的返回值类型,return[(<表达式>)]语句中表达式的值,要和函数的<数据类型>保持一致。如无返回值应写void型

  <形式参数说明>是逗号分割的过个变量的说明形式

  大括号对{<语句序列>},称为函数体;<语句序列>是大于等于零个语句构成的

  函数的说明就是指函数原型,其中<形式参数说明>可以缺省说明的变量名称,但类型不能缺省

  例如:

    double Power(double x, int n);

    double Power(double, int);

函数的使用也叫函数的调用,形式如下:

  实际函数名((实际参数))

    实参就是在使用函数时,调用函数传递给被调用函数的数据,需要确切的数据

    函数调用可以作为一个运算量出现在表达式中。也可以单独形成一个语句,对于无返回值的函数来讲,只能形成一个函数调用语句

编写一个函数打印 "Hello , guy!"

#include <stdio.h>
void DisplayHello(){
    printf("Hello,guy!\n");
}

int main(void){
    DisplayHello();
    return 0;
}

定义求x的n次方值的函数(x是实数,n为正整数)

// #include <stdio.h>
int printf(const char *format, ...);
int scanf(const char *format, ...);

double power(double x, int n);//函数说明必须在函数调用之前,但是函数实现可以在调用函数之后

int main(void){
    double x, ret;
    int n;
    printf("input:\n");
    scanf("%lf%d", &x, &n);
    ret = power(x, n);
    printf("%lf %d = %lf\n", x, n, ret);
    return 0;
}
double power(double x, int n){
    double ret = 1;
    for (int i=1; i <= n; i++){
        ret *= x;
    }
    return ret;
}

函数的参数传递

  函数之间的参数传递方式:

    1、全局变量

      全局变量就是在函数体外面声明的变量,它在程序中的每个函数里都是可见的

      全局变量一经定义后就会在程序的任何地方可见,函数调用的位置不同,程序的执行结果可能会受到影响。不建议使用

    2、复制传递

      调用函数将实参传递给被调用函数,被调用函数将创建同类型的形参并用实参初始化

      形参是新开辟的存储空间,因此,在函数中改变形参的值,不会影响到实参

    3、地址传递

      按地址传递,实参为变量的地址,而形参为同类型的指针

      被调用函数中对形参的操作,将直接改变实参的值(被调用函数对指针的目标操作,相当于对实体本身的操作)

#include <stdio.h>


void swap(int *, int *);

int main(void){
    int a = 10;
    int b = 20;
    printf("before:%d %d\n", a, b);
    swap(&a, &b);
    printf("after:%d %d\n", a, b);
    return 0;
}

void swap(int *x, int *y){
    int temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

编写函数,统计字符串中小写字母的个数,并把字符串的小写转换为大写

#include <stdio.h>

int str_fun(char *);

int main(void){
    char s[] = "hello 2020 world";
    int n, i, num;
    num = sizeof(s) / sizeof(char);
    n = str_fun(s);
    printf("num: %d\n", n);
    for(i=0; i<num; i++){
        printf("%c", s[i]);
    }
    puts("");
    return 0;
}
int str_fun(char * p){//char * p = s;
    int num = 0;
    while (*p != '\0'){//while (*p)
        if (*p <= 'z' && *p>='a'){
            *p -= ' ';//*p -=32;
            num++;
        }
        p++;    
    }
    return num;
}

数组在函数中的传参

  1、全局数组传递方式

  2、复制传递方式

    实参为数组的指针,形参为数组名(本质是一个指针变量)

  3、地址传递方式

    实参为数组的指针,形参为同类型的指针变量

写一个函数,计算一个一维数组的和

#include <stdio.h>

//int get_sum(int [], int);//复制传递
int get_sum(int *, int);//地址传递

int main(void){
    int a[] = {1,2,3,4,5};
    int sum;
    int len = sizeof(a)/sizeof(a[0]);
    sum = get_sum(a, len);
    printf("%d\n", sum);
    return 0;
}
//int get_sum(int p[], int len){//int p[] = a,error; int *p = a;
int get_sum(int *p, int len){//int *p = a;
    int sum = 0;
    //数组传递到函数内,无法获取数组的长度
    //int len = sizeof(p) / sizeof(p[0]);
    for (int i = 0; i < len; i++){
        sum += *p;
        p++;
    }
    return sum;
}

编写函数,删除字符串的空格

#include <stdio.h>

void del_space(char *);

int main(void){
    char str[] = "hello world!";
    printf("before: %s\n", str);
    del_space(str);
    printf("after: %s\n", str);
    return 0;
}
void del_space(char * s){
    // p, s指针同时指向字符串的起始位置,p,s指针一起往前走,当遇到空格是时p指针不动,并且修改该处的值为s指针现在指向位置的数值
    char * p = s;
    while(*s){
        if(*s != ' '){
            *p = *s;
            p++;
        }
        s++;
    }
    *p = '\0';
}

指针函数

  指针函数是指一个函数的返回值为地址量的函数

  指针函数的定义的一般形式如下:

  <数据类型> * <函数名称>(<参数说明>){

    语句序列;

  }

  返回值:全局变量地址/static变量地址/字符床常量地址//堆的地址

#include <stdio.h>
#include <string.h>
char * mystring(){
    char str[20];
    strcpy(str, "Hello");
    return str; //function returns address of local variable,返回局部变量地址,在其他函数中无法使用
};
int main(void){
    printf("%s\n", mystring());
    return 0;   
}
#include <stdio.h>
#include <string.h>
//char str[20];修改方式1,将str定义为全局变量
char * mystring(){
    //char * str = "Hello";//修改方式2:把str定义为字符串常量
    static char str[20];//修改方式3,最佳方式,
    strcpy(str, "Hello");
    return str;
};

int main(void){
    printf("%s\n", mystring());
    return 0;

}

 Make

make简介

  工程管理器,顾名思义,是指管理较多的文件

  Make工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作

  Make将只编译改动的代码文件,而不用完全编译

Makefile基本结构

  Makefile是Make读入的唯一配置文件

  由make工具创建的目标体(target),通常是目标文件或可执行文件

  要创建的目标体所依赖的文件(dependency_file)

  创建每个目标是需要运行的命令(command)

  注意:命令行前面必须是一个“TAB键”,否则编译错误为:*** missing separator. Stop

  Makefile格式:

    target :dependency_file

    <TAB> command

  例子:

    hello.o : hello.c hello.h

      gcc -c hello.c -o hello.o

  一个复杂一些的例子:

    sunq: kang.o yul.o

      gcc kang.o yul.o -o sunq

    kang.o: kang.c kang.h

      gcc -Wall -O -g -c kang.c -o kang.o

    yul.o:  yul.c

      gcc -Wall -O -g -c yul.c -o yul.o

    注释:

      -Wall:表示允许发出gcc所有有用的报警信息,  

      -c:只是编译不链接,生成目标文件“.o”

      -o file: 表示把输出文件输出到file里 

./
├── clean , 伪目标
├── f1.c
├── f2.c
├── head.h
├── main.c
└── Makefile

test: f1.o f2.o main.o
    gcc f1.o f2.o main.o -o test
f1.o: f1.c
    gcc -c -Wall f1.c -o f1.o
f2.o: f2.c
    gcc -c -Wall f2.c -o f2.o
main.o: main.c
    gcc -c -Wall main.c -o main.o
.PHONY:clean #为了解决伪目标(在该目录下有clean文件时,会影响make clean命令) 
clean:
    rm *.o test

创建和使用变量

  创建变量的目的:

    用来代替一个文本字符串:

      1、一系列文件的名字

      2、传递给编译器的参数

      3、需要运行的程序

      4、需要查找源代码的目录

      5、需要输出信息的目录

      6、想要做的其他事情

  变量定义的两种方式:

    1、递归展开方式:VAR = var

      foo=$(bar)

      var=$(ugh)

      ugh=Huh

      此时$(foo)的值为Huh

      优点:它可以向后应用变量

      缺点:不能对该变量进行任何展开,例如:CFLAGS = $(CFLAGS)  -o 会造成死循环

    2、简单方式:VAR : = var

      m := mm

      x :=$(m)

      y := $(x)bar

      x := later

      此时$(x) $(y)分别表示later, mmbar

      用这种方式定义变量,会在变量的定义点,按照被应用的变量的当前值进行展开

      这种定义变量的方式更适合在大的编程项目中使用,因为它更像我们一般的编程语言      

  变量使用$(VAR)、用"$$"表示"$"、类似于编程语言中的宏

  例子:

    OBJS = kong.o yul.o

    CC = gcc

    CFLAGS = -Wall -O -g

    sunq: $(OBJS)

      $(CC) $(OBJS) -o sunq

    kang.o: kang.c kang.h

      $(CC) $(CFLAGS) -c kang.c -o kang.o

    yul.o:  yul.c

      $(CC) $(CFLAGS) -c yul.c -o yul.o

  用?=定义变量

    FOO ?=bar 含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前定义过。那么条件语句将什么也不做,其等价于:

      ifeq($(origin FOO), undefined)

        FOO = bar

      endif

  为变量添加值

    可以通过+=为已定义的变量添加新的值

      Main = hello.o hell-1.o

      Main += hello-2.o

  预定义变量

    AR:库文件维护程序的名称,默认值为ar,AS汇编程序的名称,默认值为as

    CC:C编译器的名称,默认值为cc,CPP  C预编译器的名称,默认值为$(CC) -E

    CXX:C++编译器的名称,默认值为g++

    FC:FORTRAN编译器的名称,默认值为f77

    RM:文件删除程序的名称,默认值为rm -f

  自动变量

    $*:不包含扩展名的目标文件名称

    $+:所有的依赖文件,以空格分开,并以出现的先后为顺序,可能包含重复的依赖文件

    $<:第一个依赖文件的名称

    $?:所有时间戳比目标文件晚的依赖文件,并以空格分开

    $@:目标文件的完整名称

    $^:所有不重复的目标依赖文件,以空格分开

    $%:如果目标是归档成员,则该变量表示目标的归档成员名称

test: f1.o f2.o main.o
    gcc f1.o f2.o main.o -o test
    $(warning $*)
    $(warning $+)
    $(warning $<)
    $(warning $?)
    $(warning $@)
    $(warning $^)
    $(warning $%)
f1.o: f1.c
    gcc -c -Wall f1.c -o f1.o
f2.o: f2.c
    gcc -c -Wall f2.c -o f2.o
main.o: main.c
    gcc -c -Wall main.c -o main.o
.PHONY:clean #为了解决伪目标(在该目录下有clean文件时,会影响make clean命令) 
clean:
    rm *.o test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ gcc -c -Wall f1.c -o f1.o gcc -c -Wall f2.c -o f2.o gcc -c -Wall main.c -o main.o Makefile:
2: Makefile:2: f1.o f2.o main.o Makefile:2: f1.o Makefile:2: f1.o f2.o main.o Makefile:2: test Makefile:2: f1.o f2.o main.o Makefile:2: gcc f1.o f2.o main.o -o test

  环境变量

    make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量

    如果用户在Makefile中定义的相同名称的变量,那么用户定义变量将会覆盖同名的环境变量

  Make的使用

    直接运行make,选项:

      -C:dir读入指定目录下的Makefile

      -f:file读入当前目录下的file文件作为Makefile

      -i:忽略所有的命令执行错误

      -I:dir指定被包括的Makefile所在目录

      -n:只打印要执行的命令,但不执行这些命令

      -p:显示make变量数据库和隐含规则

      -s:在执行命令时不显示命令

      -w:如果make在执行过程中改变目录,打印当前目录名

VPATH

  当make在当前路径下找不到依赖文件时,会去VPATH下一次寻找,例如:

  VPATH = src1 src2 , 一次去src1目录下寻找,然后再去src2目录下寻找

Makefile的嵌套

条件编译

编译器根据条件的真假决定是否编译相关的代码

常见的条件编译有两种方法

  1. 根据宏时候定义,其语法如下

    #ifdef <macro>

      ......

    #else

      ......

    #endif

  2.根据宏的值,其语法如下:

    #if <macro>

      ......

    #else

      ......

    #endif

#include <stdio.h>

#define _DEBUG_
#define N 10

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

#ifdef _DEBUG_
    printf("_DEBUG_\n");
#else
    printf("release\n");
#endif

#if N
    printf("N:%d\n", N);
#else
    printf("aaa\n");
#endif

}

结构体

定义一个结构体类型的一般形式

  struct 结构体名

  {

  数据类型   成员1;

  ......

  数据类型  成员n;

  };

在大括号中的内容也称为“成员列表”或“域表”

其中,每个成员的命名规则与变量名相同

数据类型可以是基本变量类型和数据类型,或者是一个结构体类型

用分号";"作为结束符。整个结构的定义也用分号作为结束符

说明:

  结构体类型中成员可以与程序中的变量名相同,二者并不代表同一个对象,编译程序可以自动对他们进行区分

  结构体类型的特征

  1. 结构体类型是用户自行构造的
  2. 它由若干不同的基本数据类型的数据构成
  3. 它属于C语言的一种数据类型,与整型,实型相当,因此,定义时不分配空间,只用用它定义变量时才分配空间

结构体类型变量的定义方法

  1、先定义结构体再定义变量

    struct 结构体名

    {};

    struct 结构名 变量名;

  2、在定义类型的同时定义变量

    struct 结构体名

    {} 变量名;

  3、直接定义结构体变量

    struct 

    {} 变量名;

大小

  一个结构体变量占用内存的实际大小,也可以利用sizeof求出,运算表达式:

    sizeof(运算符)//求出运算符占用内存空间的字节数

    运算符可以是变量,数组或结构体变量,可以是数据类型的名称。

#include <stdio.h>

#define N 20
#define STUDENT struct student
struct student {
    int no;
    char name[N];
    float score;
}s3, s4;
int main(int argc, const char *argv[]){
    STUDENT s1, s2;
    printf("%ld\n", sizeof(struct student));//28
    printf("%ld\n", sizeof(STUDENT));//28
    printf("%ld\n", sizeof(s1));//28
    return 0;
}

结构体变量的赋值和初始化

  初始化,在定义结构体变量的可以使用{}统一赋值,在定义结构体变量之后只能单独赋值

#include <stdio.h>
#include <string.h>

#define N 20
#define STUDENT struct student
struct student {
    int no;
    char name[N];
    float score;
}s3 = {3"s3", 90}, s4 = {4, "s4", 89};
int main(int argc, const char *argv[]){
    STUDENT s1, s2;
    s1.no = 1;
    strcpy(s1.name, "s1");
    s1.score = 90;

    s1.score = 99;
    s1.name[0] = 'S';
    printf("%d %s %.2f\n", s1.no, s1.name, s1.score);
    s2 = s1; // 相同类型的结构体变量之间可以赋值
    s2.name[1] = '2';
    s2.no = 2;
    printf("%d %s %.2f\n", s1.no, s1.name, s1.score);// s1,s2占有单独的空间
    printf("%d %s %.2f\n", s2.no, s2.name, s2.score);
    return 0;
}

结构体嵌套使用

#include <stdio.h>
#include <string.h>

#define N 20
#define STUDENT struct student


struct birthday {
    int year;
    int month;
    int day;
};

struct student {
    int no;
    char name[N];
    struct birthday data;
    float score;
}s1={1, "s1", {1992, 10, 10}, 100};

int main(int argc, const char *argv[]){
    STUDENT s2;
    s2.no = 2;
    strcpy(s2.name, "S2");
    s2.data.year = 1992;
    s2.data.month = 9;
    s2.data.day = 20;
    s2.score = 99;
    printf("%d %s %d-%d-%d %.2f\n", s1.no, s1.name, s1.data.year, s1.data.month, s1.data.day, s1.score);
    printf("%d %s %d-%d-%d %.2f\n", s2.no, s2.name, s2.data.year, s2.data.month, s2.data.day,s2.score);
    return 0;
}

结构体数组

  具有相同结构体类型的变量的集合,称为结构体数据

  定义结构体数据的方法和定义结构体变量的方法相仿,只需要说明其为数组即可

  可以采用三种方法

  1. 先定义结构体类型,再用它定义结构体数组
  2. 在定义结构体类型的同时定义结构体数组
  3. 直接定义结构体数组
#include <stdio.h>
#include <string.h>

#define N 20
#define STUDENT struct student

struct student {
    int no;
    char name[N];
    float score;
}s2[5];


int main(int argc, const char *argv[]){
    int i ; 
    STUDENT s1[] = {{1, "S1", 90, },{2, "S2", 99},{3, "S3", 89}};
    //printf("%d-%s-%.2f\n", s1[0].no, s1[0].name, s1[0].score);
    s2[0] = s1[0];
    s2[1] = s1[1];
    s2[2] = s1[2];
    for (i = 0; i < sizeof(s1)/sizeof(s1[0]); i++ ){
        printf("%d-%s-%.2f\n", s1[i].no, s1[i].name, s1[i].score);
    }
    for (i = 0; i < sizeof(s2)/sizeof(s2[0]); i++ ){
        printf("%d-%s-%.2f\n", s2[i].no, s2[i].name, s2[i].score);
    }
    return 0;
}

结构体与指针

  struct 结构体名 * 指针名

#include <stdio.h>

#define STUDENT struct student

struct student {
    int no;
    char name[N];
    float score;
};

int main(int argc, const char *argv[]){
    int i ; 
    STUDENT s1[] = {{1, "S1", 90, },{2, "S2", 99},{3, "S3", 89}};
    STUDENT * p;
    p = s1;// p = &s1[0]
    //通过指针变量->成员获取值
    printf("---%d-%s-%.2f\n", p->no, p -> name, (*p).score);
    for (i = 0; i < sizeof(s1)/sizeof(s1[0]); i++ ){
        printf("%d-%s-%.2f\n", p->no, p -> name, (*p).score);
        p++;
    }

    return 0;
}

共用体

  在C语言中,不同数据类型可以使用共同的存储区域,这种数据构造类型称为共用体,简称共用,又称联合体。共用体在定义

说明和使用形式上与结构体相似,两者本质上的不同仅在于使用内存的方式上。

  定义一个共用体类型的一般形式:

    union 共用体名

    {

      成员列表;

    };

  共用体占用内存空间与内存最大的成员变量相同;结构体占用的内存空间是所有成员的内存之和

#include <stdio.h>

union gy {
    char a;
    short b;
    int c;
}v2;


int main(int argc, const char *argv[]){
    union gy v1;
    char *p;
    int i;

    printf("%ld %ld %ld %ld\n", sizeof(char), sizeof(short), sizeof(int), sizeof(v1));// 1 2 4 4
    v1.a = 'A';
    v1.b = 20;
    v1.c = 0x12345678;//成员a b的赋值已经无效
    printf("%#x %c\n", v1.a, v1.a);//0x78 x
    printf("%p %p %p\n", &v1.a, &v1.b, &v1.c);//0x7fffc6bb0d64 0x7fffc6bb0d64 0x7fffc6bb0d64
    
    p = (char *)&v1;
    for (i=0; i<sizeof(v1); i++){
        printf("%#x\n", *p);
        p++;
    }
/* 
0x78
0x56
0x34
0x12
小端:低地址存放低位
*/
    return 0;
}

typedef

在C语言中,允许使用关键字typedef定义新的数据类型

其语法如下:

  typedef <已有数据类型> <新数据类型>;

  typedef int INTEGER

  这里定义了新的数据类型INTEGER,其等价于int

在C语言中经常在定义结构体类型时使用typedef,例如:

  typedef struct _node_{

    int data;

    struct _node_ *next;

  }listnode, *linklist

  在这里定义了两个新的数据类型listnode、linklist,分别等价于struct _node_ 、struct _node_ *

内存管理

C/C++定义了4个内存区间

  代码区、 全局变量与静态变量区、局部变量区即栈区、动态存储区即堆区

静态存储分配

  通常定义变量,编译器在编译时就可以根据该变量的数据类型知道所需内存空间大小,从而系统在适当的时候为他们分配确定的存储空间。

  静态存储的三种方式

    1、全局变量

    2、静态变量,static关键字修饰的变量

    3、字符串常量

在栈上创建

  在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率高,但是分配的内存容量有限(ulimit -s 查看stack size,8192M)

动态存储分配

  有些操作对象只有在程序运行时才确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序执行时,系统根据运行时的要求进行内存分配

  所有动态存储分配都在堆区中进行

  从堆上分配,亦称动态内存分配。程序在运行的时候调用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存,动态内存的生存期由我们决定,使用非常灵活,但问题也最多

malloc/free

  void * malloc(size_t num): 申请num字节大小的内存,返回值是内存的起始地址

  void free(void *p):p为申请内存的起始地址

  malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数

  malloc申请到的是一块连续的内存,有时候可能会比所申请内存的空间大。有时候会申请不到内存,返回NULL

  malloc返回值的类型是void,所以在调用malloc时要显式地进行类型转换,将void * 转换为所需要的指针类型

  如果free的参数是NULL的话,没有任何效果

  释放一块内存中的一部分是不被允许的

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

int main(int argc, const char *argv[]){
    char *p;
    p = (char *)malloc(10 * sizeof(char));
    if (p == NULL){
        printf("malloc failed\n");
        return 0;
    }
    printf("p=%p\n", p);
    printf("input:");
    scanf("%s", p);
    printf("p=%s\n", p);
    free(p);
    while(1){
    
    }
    return 0;
}

 

 

 

 

 

 

 

 

posted @ 2019-04-14 11:58  xdl_smile  阅读(487)  评论(0编辑  收藏  举报