C语言入门


Hello World

    #include <stdio.h> //标准输入输出库
    #include <stdlib.h> //标准lib,包括system等函数

    int main(void)  //主函数声明格式
    {

        //双斜杠(//)单行注释
        // /*+内容+*/,多行注释
        printf("Hello,World");  //打印字符串

        system("pause");    //暂停程序,好观察结果
        return 0;   //程序结束
    }

数据类型

整型

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

    int main(void)
    {
        int a;  // 声明一个整型,默认为有符号类型即signed,4 bytes
        printf("%d",a); // 十进制形式输出有符号整数


        unsigned int b; // 声明一个无符号整型,4 bytes
        printf("%u",b); // 十进制形式输出无符号整数


        short int c;   // 声明一个短整型,默认为有符号类型, 2 bytes
        printf("%hd",c);    // 十进制输出短整型


        long int d;     // 声明一个长整型,默认为有符号类型, 8 bytes
        printf("%ld",d);    // 十进制输出长整型


        long long int e;    // 声明长长整型,默认为有符号, 16 bytes,c99之后的拓展
        printf("%lld",e);   // 十进制输出长长整型

        printf("%p",&a); // 以十六进制输出变量a的地址,不带前缀0x
        printf("%#p",&a);   // 以十六进制输出变量a的地址,带前缀0x
    }

scanf获取输入

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

    int main(void)
    {
        int a;
        int b;

        scanf("%d",&a);     //获取输入赋值给a,输入以回车(换行)为结束符

        scanf("%d%d",&a,&b);    //同时获取多个输入时,输入用空格隔开

        scanf("%d,%d",&a,&b);   //同时获取多个输入时可以以逗号或者其他字符隔开,即转义字符中间用什么隔开,输入的时候就用什么隔开
        system("pause");
        return 0;
    }

忽略warning 方案一:文件首行添加宏定义: #define _CRT_SECURE_NO_DEPRECATE 方案二:添加:#pragma warning(disable:4996)

    #define _CRT_SECURE_NO_DEPRECATE

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

    #pragma warning(disable:4996)
    //4996为warning的“编号”

    int main(void)
    {
        system("pause");
        return 0;
    }

for循环

for(;;) // 死循环
for(i = 0;i < 10;i++)
/*
执行过程,
1. i = 0为初始值设置,在进入for循环时执行一次
2. i<10,为状态检测,每一轮循环开始都会执行这一部分
3. i++,状态更新,循环体内容执行结束之后执行这部分内容,更新for循环状态
*/

逗号表达式

for(i = 1, cost = N; i <= 16 ; i++, cost += 20)
    printf("test for code 'for;\n");

逗号的性质:

  1. 保证了被他分隔的表达式从左往右求值(换言之,逗号是一个序列点,所以逗号左侧项的所有副作用都在程序执行逗号右侧之前发生)

     ounces++, cost = ounces*20;
     // 先递增ounces,然后在第二个表达式中使用ounces的新值,然后赋值给cost
    
  2. 整个逗号表达式的值是右侧项的值

     x = (y = 3, (z = ++y + 2) + 5);
     // 先把3赋给y,递增y为4,然后把4+2之和赋给z,最后加上5,然后把结果赋给x
     x = 249,500;
     // 等价于 x = 249; 500; 其中,500为一个表达式(do nothing)
     x = (249,500);
     // (249,500) 的值为500,然后赋给x
    

数组

  1. 出于速度原因,编译器不会对数组做越界检测,这样导致的问题是修改或读取程序其他数据,可能会破环程序结果甚至导致程序异常中断。

  2. 如果char类型的数组结尾包含了一个表示字符串末尾的空字符 ‘\0’,则该数组中的内容就构成了一个字符串。

  3. 数组由相邻的内存位置构成,只储存相同类型的数据。

逻辑运算

c99 新增了可替代逻辑运算符的拼写,被定义在 ios646.h中

传统写法 ios646.h
&& and
|| or
! not

sizeof 和 size_t

  1. sizeof用于以字节为单位返回运算对象的大小,运算对象可以是具体的数据对象或类型。如果运算对象是类型,则必须用圆括号括起来(如:sizeof(int))
  2. size_t: c语言中规定,sizeof的返回类型为size_t

size_t是一个无符号整型,但不是新类型。在c文件系统中使用typedef 把size_t作为 unsigned int 或 unsigned long 的别名

ctype.h 中的字符测试函数

函数名 返回为真的参数情况
isalnum() 字母或数字
isalpha() 字母
isblank() 标准空白字符(空格、水平制表符或换行符)或任何其他本地化指定为空白的字符
iscntrl() 控制字符,如ctrl+B
isdigit() 数字
isgraph() 除空格之外的任意可打印字符
islower() 小写字母
isprint() 可打印字符
ispunct() 标点符号(除空格或字母数字字符意外的任何可打印字符)
isspace() 空白字符(空格、换行符、换页符、回车符、垂直制表符、水平制表符或其他本地化定义的字符)
isupper() 大写字母
isxdigit() 十六进制数字字符
函数名 行为
tolower() 如果参数是大写字符,返回参数的小写字母,否则返回原始参数
toupper() 如果参数是小写字母,返回参数的大写字母,否则返回原始参数

输入与输出

缓冲区

while((ch = getchar()) != '#')
put(ch)

无缓冲:上述代码中回显用户输入后立即打印输入的情况就是无缓冲

缓冲分为两类:完全缓冲I/O和行缓冲I/O

  1. 完全缓冲是指当缓冲区被填满时才刷新缓冲区,通常出现在文件输入中
  2. 行缓冲则是在出现换行符时刷新缓冲区,键盘输入通常是行缓冲

文件输入与重定向

c程序处理的是流而不是直接处理文件,流(stream)是一个从实际输入或输出映射的理想化数据流,即不同属性和不同种类的输入都有更统一的流来表示。

文件结尾

计算机操作系统可以通过检测文件结尾来判断文件的结束,即在文件末尾放置一个特殊的字符。有些操作系统利用内嵌的 Ctrl+Z(CTRL +D)来标记文件结尾。

在c语言中,用getchar()读取文件检测到文件结尾时返回一个特殊值,即EOF(end of file),scanf同样如此。

通常,EOF定义在 stdio.h文件中 #define EOF(-1)

while((ch == getchar() != EOF))

上述表达式可以检测是否到达文件结尾,对于不是读取文件而是键盘输入的情况,大多数系统可以通过键盘模拟文件结尾条件。

重定向

假定存在一个已编译的程 test 和文本文件 file1,file2

$ test > file1  
// 将test的输出写入到file1
$ test < file2  
// 将file2的内容作为输入给test

对于 windows,Linux,Unix, >< 两侧的空格是可选项。

关于重定向运算符的两个原则:

  1. 重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接两个可执行文件
  2. 使用重定位运算符不能读取多个文件的输入,也不能把输出重定向至多个文件
$ test < file1 > file2
// 指令合法
$ test > file2 < file1

命令与重定向运算符的顺序无关,但是在一条指令中输入文件名和输出文件名不能相同

函数

函数原型

函数原型声明:

int imax(int, int);
int imax(int a, int b);

上例中,如果两个参数都是数字但是类型不匹配,编译器会把实际参数的类型转化为形式参数的类型,但是这种转换往往会因为精度问题造成数据丢失。

一般情况下,为表明函数确实没有参数,应该在圆括号中使用关键字void:

void print_name(void);

支持ANSI C的编译器解释为该函数不接受任何参数,在调用是会对其进行检查,确保没有使用参数。此外,对于如printf等函数有许多参数的情况,可以有以下函数原型:

int printf(const char *, ...);

函数原型可以让编译器捕获在使用函数时可能出现的许多错误或疏漏。此外,有一种省略函数原型却保留其有点的例子:

int imax(int a, int b) {return a > b ? a: b; }
int main()
{....}

即把整个函数的定义放在第一次调用函数之前,也可以起到和函数原型一样的效果。

递归

递归与循环:一般而言,选择循环要优于递归:

  1. 每次递归都会创建一组变量,所以递归使用的内存多,而且每次递归调用都会创建一组新的变量放在栈中。所以递归调用的数量受限于内存空间。
  2. 由于每次函数调用都要花费一定的时间,所以递归的执行速度较慢

递归和逆序计算:递归在处理倒时序时较之循环要方便许多。

假设设计一个以二进制形式表示整数的函数,则使用递归的方法可以有:

void 2binary(unsigned long n)
{
    int r;
    r = n % 2;
    if(n >= 2)
        2bianry(n/2);
    putchar(r == 0 ? '0' : '1');
    return;
}

技巧性编程: while(scanf("%lu", &num) == 1), 在循环获取输入时,可以直接以输入函数的返回值作为循环结束的条件,此例中,如果输入的不是一个整数而是字符等其他类型,那么while循环就会终止 scanf("%*s")可以直接跳至下一个空白字符,如果程序要求获得整数的数据,那么利用这个写法可以过滤掉输入中非整数的输入

多文件编译

gcc fiel1.c file2.c
gcc file1.o file2.o

绝大多数的DOS命令行编译器的工作原理和UNIX的cc命令类似。

指针

*和指针名之间的空格可有可无,通常,在声明时使用空格,在解引用时省略空格

函数签名: 函数的返回类型和形参列表构成了函数签名,因此,函数签名指定了传入函数参数的值以及函数返回值的类型。

数组与指针

数组初始化:

int powers[8] = {1, 2, 4, 6, 8, 16, 32, 64};

ANSI C开始支持这种初始化,不过不支持ANSI的编译器会把这种形式的初始化识别为语法错误,在数组申明前再加上关键字static可以解决此问题。

const声明数组,可以把数组设置为只读,这样一来,程序只能从数组中检索值,不能把新值写入数组。

如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。

指定初始化器(designated initalizer): 这是c99增加的新特性,即可以在初始化列表中使用带方括号的下标指明待初始化的元素(int arr[6] = {[5] = 212}; // 把arr[5]初始化为212)

初始化器的两个特性:

  1. 如果指定初始化器后门有更多的值,如[5]=31,32,33,那么在下标为5的元素初始化为31之后,随后的元素也会被初始化为对应的值。
  2. 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化,如:[5]=31,32,[6]=33;数组下标6的值会是33。

如果未指定元素的大小,如:

int stuff[] = {1, [6] = 23};
int staff[] = {1, [6] = 4, 9, 10};

编译器会把下标的大小设置为足够装得下初始化的值,所以stuff有7个元素,staff有9个。

注意: 1.c不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值 2.编译器不会检查数组下标是否使用得当,在c标准中,使用越界下标的结果是未定义的即程序看上去可以运行,但是运行结果很奇怪。 3.使用越界的数组下标会导致程序会导致程序改变其他变量的值,不同编译器运行该程序的结果可能不同,有些会导致程序异常终止。

c99之前,声明数组时只能在方括号中使用整型常量表达式(由整型常量构成的表达式,sizeof表达式被视为整型常量,但是与c++不同的是const不是。

int n = 5;
int m = 8;
float a[n];
float b[m];

上述写法中,在支持c90的编译器是不允许的,但是c99允许,这种创建了一种新型数组即变长数组(variable-length array,VLA)。不过c11放弃了这一创新的举措,把VLA设置为可选而不是语言必备的特性。c99引入变长数组主要是为了让c成为更好的数值计算语言。

int date[10];
dates + 2 == &date[2];
*(dates + 2) == date[2];

指针加1,指针的值递增它所指向类型的大小(以字节为单位)。

对于函数的形参,只有在函数原型或函数定义中,才可以用int ar[]代替int * ar,int ar[] 和 int * ar都表示ar是一个指向int的指针,但是 inr ar[]只能用于声明形式参数,而int * ar的ar不仅仅是一个int类型的值,还是一个int类型的数组的元素

指针操作

  1. 赋值: 可以把地址赋值给指针
  2. 解引用: *运算给出指针指向地址上存储的值
  3. 取址:和所有变量一样,指针变量也有自己的地址和值
  4. 指针与整数相加:可以使用+运算把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向的类型的大小(以字节为单位)相乘,然后把结果与初始地址相加,即ptr + 4&urn[4](ptr指向数组urn)等价
  5. 递增指针:递增指向数组元素的指针可以上该指针移动至数组下一个元素
  6. 指针减去一个整数:可以使用-运算符从一个指针中减去一个整数,与加类似
  7. 递减指针:与递增指针类似
  8. 指针求差:可以计算两个指针的插值,差值的单位与数组类型的单位相同
  9. 比较使用比较关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象

注意: 1.指针的减法有两种,可以用一个指针减去另一个指针得到一个整数,也可以用一个指针减去一个整数得到另一个指针 2.创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存,所以在使用指针前必须先用已分配的地址初始化它

int sum(const int ar[], int n);

上述函数原型申明参数列表中,const关键字告诉编译器该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似a[i]++的表达式,编译器会捕获这个错误,并生成错误信息。

指向多维数组的指针:

int(* pz)[2];

上述的声明把pz指向一个数组的指针,这个数组内含有两个int类型的值(注意,[]的优先级要高于*

c const 和 c++ const: 二者用法相似,但不完全相同。区别一,C++允许申明数组大小时使用const整数,而c不允许;区别二,c++的指针赋值检查更严格

变长数组(VLA)

#define COLS 4
int sum2d(int ar[][COLS], int rows);
{
    ...
}
/*
* 假定声明以下数组:
* int array1[5][4];
* int array2[100][4];
* int array3[2][4];
* 则这三个数组都可以传递给函数sum2d
*/

但是如果要处理任意大小的二维数组,这个函数就不适用了。不过c99新增的变长数组允许使用变量表示数组的维度:

int quarters = 4;
int regions = 5;
double sales[regions][quarters];

注意:变长数组中的“变”不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小就保持不变。这里的“变”表示在创建数组时,可以使用变量指定数组的维度。

复合字面量

(int [2]){10,20}
(int []){50,20,90}

可以看出复合字面量实际上是匿名的,所以不能创建它然后再使用它,也就是说必须在创建时就使用,如:

int * pt1;
pt1 = (int [2]){10,20};

或者作为函数参数传入:

int sum(const int ar[], int n);
...
int total;
total = sum((int[]){4, 4, 4, 5, 6, 7}, 6);

字符串

  1. 字符串字面量(字符串常量):用双引号括起来的内容称为字符串字面量(string literal),也叫字符串常量(string constant)。双引号中的字符和编译器自动加入末尾的\0字符都作为字符串存储在内存中。
  2. 从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分割,c会将其视为串联起来的字符串字面量如:

     char temp[] = "abcdef"" ghij kl" "mn"
     "  opq";
    
  3. 字符串常量属于静态存储类别(static storage class),即如果在函数中使用字符串常量,该字符只会被存储一次,在整个程序的生命周期内存在,即使函数被调用多次。
  4. 在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符),所有未被使用的元素被自动初始化为\0
  5. 字符串存储在静态存储区,但是,程序开始运行时才会为该数组分配内存 ,然后将字符串拷贝到数组中
  6. 关于”Segmentation fault”段错误,在Unix系统中表示该程序试图访问未分配的内存

存储类别、链接和内存管理

存储类别

c提供了多种不同的模型或存储类别(storage class)在内存中储存数据,从硬件方面来看,被存储的每个值都占用一定的物理内存,c语言把这样的一块内存称为对象(object),。对象可以存储一个或多个值。

作用域

  1. 作用域是描述程序中可访问标识符的区域
  2. 块是用一对花括号括起来的代码区域
  3. 定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含改定义的块的末尾。此外,虽然函数的形式参数声明在函数的左花括号之前,但是他们也具有块作用域,属于函数体这个块
  4. 函数原型作用域的范围是从形参定义处到原型声明结束,编译器在处理函数原型中的形参时只关心他的类型,而形参名通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配,只有在变长数组中,形参名才有用:void use_a_VLA(int n. int m, ar[n][m]);,方括号中必须使用在函数原型中已声明的名称
  5. 变量的定义在函数外面,具有文件作用域(file scope)。具有文件作用域的变量,从它定义到该定义所在文件的末尾均可见

链接

  1. 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。也就意味着这些变量属于定义他们的块、函数或原型私有。
  2. 具有文件作用域的变量可以是外部链接或内部链接。
  3. 外部链接变量可以在多文件程序中使用,内部链接只能在一个翻译单元中使用。
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3 // 文件作用域,内部链接
int mian()
....

存储期

  1. 作用域和链接描述了标识符的可见性。存储器描述了通过这些标识符访问的对象的生存期。c对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
  2. 如果对象具有静态存储期,那么它在程序执行期间一直存在。文件作用域变量具有静态存储期。无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
  3. 块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚刚为变量分配的内存。
  4. 变长数组的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。
  5. 块作用域也能具有静态存储期,只需要把变量声明在块中,然后加上关键字static

内存管理

  • malloc函数,接收一个参数(所需内存的字节数)

    malloc函数在执行时会找到一块合适的匿名空闲内存块,即malloc在分配内存时不会为其赋名,它的返回值为动态分配内存块的首字节地址。因此,可以把地址赋给一个指针变量,并使用指针访问这块内存。

    char表示一字节,malloc的返回类型通常被定义为指向char的指针。不过,从ANSI C开始,C使用指向void的指针,相当于一个通用指针,malloc函数可用于返回指向数组的指针、指向结构的指针等,通常该函数的返回值会被强制转换为匹配的类型。此外,把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题

    通常,malloc要和free配合使用,free函数的参数是malloc返回的地址,即释放malloc申请的内存。

    静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非使用free释放。

    自动变量:具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。也可以加关键字 auto

  • calloc函数,与malloc类似

    在ANSI C之前,calloc也返回指向char的指针,之后返回指向void的指针。如果要存储不同的类型,应使用强制类型转换运算符。

    calloc接收两个无符号整数作为参数,第一个是所需内存单元数量,第二个是存储单元的大小(以字节为单位)。此外,calloc会把申请的块所有位都置零,free也可以释放calloc申请的内存

文件输入/输出

exit函数

exit()函数关闭所有打开的文件并结束程序

exit()函数的参数被传递给一些操作系统,包括UNIX、Linux、Windows和MS-DOS,以提供其他程序使用。通常惯例是:正常结束的程序传递0,异常结束的程序传递非零值。不同的退出值可用于区分程序失败的不同原因,但并不是所有操作系统能识别的范围。

C规定了一个最小的限制范围,标准要求0或宏EXIT_SUCCESS用于表明成功结束,宏EXIT_FAILURE用于表明结束进程失败,这些宏和exit()原型都于stdlib.h

ANSI C规定,在最初调用的main()中使用return 和调用exit()的效果相同,即:return 0;等价于exit(0)

但是如果mian在一个递归程序中,exit仍然会终止程序,但是return只会把控制权交给上一级递归,直至最初的一级,然后return结束程序。

此外,在其他函数中调用exit也能结束整个程序。

fopen函数

Alt

c11新增了x模式:

  1. 如果以传统的一种写模式打开一个现有文件,fopen会把该文件的长度截为0,这样就丢失了该文件的内容。但是使用带字母x的写模式,即使fopen打开文件失败,原文件的内容也不会被删除。
  2. 如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件

如果使用任何一种w模式(不带x)打开一个现有的文件,该文件内容会被删除,以便程序在一个空白文件中开始操作。然而,如果使用带x的任何一种模式,将无法打开一个现有文件

程序成功打开文件后,fopen将返回文件指针,其他I/O函数可以使用这个指针指定该文件。文件类型的指针是指向FILE的指针,FILE在stdio.h中的派生类型。

fclose函数

fclose(fp)关闭fp指定的文件,必要时刷新缓冲区。如果成功关闭返回0,否则返回EOF

指向标准文件的指针

Alt

stdio.h头文件把这三个文件指针和三个标准文件相关联,c程序会自动打开这三个标准文件

如:

fprintf(stderr,"some Erro happened!");

int fflush函数

int fflush(FILE *fp);

调用fflush函数引起缓冲区中所有未写入数据被发送到fp指定的输出文件,即刷新缓冲区。如果fp指针是空指针,所有缓冲区都被刷新,在输入流中使用fflush函数的效果是未定义的。只要最近一次操作不是输入操作,就可以用该函数来更新流(任何读写模式)

int setvbuf函数

int setvbuf(FILE * restrict fp, char *restrict buf, int mode, size_t size);

setvbuf函数创建了一个供标准I/O函数替换使用的缓冲区。如果把NULL作为buf的值,该函数会为自己分配一个缓冲区。

mode的选择如下:

  1. _IOFBF: 完全缓冲(在缓冲区满时刷新)
  2. _IOLBF: 行缓冲(在缓冲区满时或写入一个换行符时)
  3. _IONBF: 无缓冲,如果操作成功,函数返回0,否则返回一个非零值

结构和其他数据形式