C语言点滴学习笔记

数据类型

原码、反码、补码

原码:最高位为符号位,最高位为0表示正数,1表示负数
反码:正数与原码一样,负数则对原码除符号位外取反得到
补码:正数与原码一样,负数则对原码除符号位外取反再加一得到
计算机中,整数使用补码表示。
若用原码表示,则a+(-a)不为零,若用反码表示,则会出现两个0的表示。用补码可解决相加不为零,出现两个0的表示的问题。

整数溢出

#include 
#include 
int main() {
    printf("INT_MAX=%d\n",INT_MAX);
    printf("INT_MAX+1=%d\n",INT_MAX+1);
    printf("INT_MAX+INT_MAX=%d\n",INT_MAX+INT_MAX);
    printf("---------------------------\n");
    printf("INT_MIN=%d\n",INT_MIN);
    printf("INT_MIN-1=%d\n",INT_MIN-1);
    printf("INT_MIN+INT_MIN=%d\n",INT_MIN+INT_MIN);
    printf("---------------------------\n");
    printf("CHAR_MIN=%d\n",CHAR_MIN);
    printf("CHAR_MAX=%d\n",CHAR_MAX);
    printf("LONG_MAX=%ld\n",LONG_MAX);
    printf("LONG_LONG_MAX=%lld\n",LLONG_MAX);
    printf("SHORT_MAX=%d\n",SHRT_MAX);
    return 0;
}

输出(不同机器上不一样)

INT_MAX=2147483647
INT_MAX+1=-2147483648
INT_MAX+INT_MAX=-2
—————————
INT_MIN=-2147483648
INT_MIN-1=2147483647
INT_MIN+INT_MIN=0
—————————
CHAR_MIN=-128
CHAR_MAX=127
LONG_MAX=2147483647
LONG_LONG_MAX=9223372036854775807
SHORT_MAX=32767

避免溢出的技巧

    int a,b;
    if((unsigned)(a)>INT_MAX) ;/*有符号正数a上溢出了*/
    if((unsigned)(a)<=INT_MAX); /*有符号负数a下溢出了*/

    if(a>INT_MAX-b) ;/*有符号数加法a+b会发生上溢出 */
    if(a/*有符号数减法a-b会发生下溢出*/

    if(a+b/*无符号数加法已经发生上溢出*/
    if(a>b) ;/*无符号数减法a-b会发生下溢出*/

尽量不要使用无符号数,不然有符号数和无符号数混用发生隐式类型转换会出现很多难以发现的问题。

浮点数

#include

printf("%e\n",FLT_MAX);//float能保存的最大范围
    printf("%e\n",FLT_MIN);//最小范围
    printf("%d\n",FLT_DIG);//有效位
    printf("%f\n",FLT_EPSILON);
    printf("%d\n",FLT_MAX_EXP);
    printf("%d\n",FLT_MIN_EXP);
    /*
3.402823e+038
1.175494e-038
6
0.000000
128
-125
*/
//FLT_EPSION的作用是比较两个浮点数
    float a=10.0000001f,b=10.000000f;
    if(fabs(a-b)printf("equals");
    else
        printf("not euqals");

sizeof

sizeof是C语言的关键字,返回类型为size_t的无符号整形。

    #include 

    char str1[] = "abcdefg";
    char str2[] = "abc";
    if (strlen(str2) - strlen(str1)>0){
        printf("str2 is longer than str1\n");
    }
    //这里仍然会打印,strlen返回size_t,因为无符号整数溢出了,所以应该这样判断
    if (strlen(str2) >strlen(str1));

因为有字节对齐,struct的长度在不同平台上可能不同,通常用sizeof来计算变量所占内存大小。

    int a[] = {0, 1, 2, 3, 4, 5};
    char c[] = {'a', 'b'};
    int *ip = a;
    char *cp = c;
    printf("sizeof(ip)=%d\n", sizeof(ip));
    printf("sizeof(cp)=%d\n", sizeof(cp));
    printf("sizeof(a)=%d\n", sizeof(a));
    printf("sizeof(c)=%d\n", sizeof(c));
    printf("sizeof(a)/sizeof(a[0])=%d\n", sizeof(a) / sizeof(a[0]));
    /*
sizeof(ip)=4
sizeof(cp)=4
sizeof(a)=24
sizeof(c)=2
sizeof(a)/sizeof(a[0])=6
*/
//对数组名是获得整个数组占用字节数;对指针,指针都只保存一个地址,所以任何类型指针都占用相同的字节数。可以用sizeof(a)/sizeof(a[0])计算数组长度。
    char* str="hello";
    printf("%s\n",str);
    char str1[]="hello";
    printf("%s\n",str1);

    char str2[2];
    str2[0]='a';
    str2[1]=0;
    printf("%s\n",str2);
/*hello
hello
ahello*/
//设置了长度的char数组要自己补0作结束符

int的长度可能与CPU字长一样,只是在现在的平台下是4字节而已,我们只能保证sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)

表达式和运算符

C 语言中只有三个半运算符,其运算顺序是固定的,分别是逻辑与运算符“&&”,逻辑或运算符“||”,还有C 语言中唯一的一个三目运算符“?:”,另外半个是逗号运算符“,”。之所以称它为半个,是因为当逗号用来分开函数的实参时,运算顺序也是不固定的。

int m = 1;
printf("%d %d", m, m=7); /* bad smell ,输出7 7*/
a[m] = m++; /* 可能给a[8]赋值 */

f() + g() * h()//哪怕加了括号,也不能保证哪个函数先运行
a=f();//所以最好用临时变量保存中间值
b=g();
c=h()
a+b*c;

表达式运算的顺序,完全是根据编译器和平台确定的。所以一个建议就是,凡是涉及到改变某个值的表达式,例如自增(减)运算符,等号等,最好能够自成一行,不要混杂在其他表达式的里面。

C语言的位图,相当于Java的boolean数组

#define BITMASK(b) (1 << ((b) % CHAR_BIT))
#define BITSLOT(b) ((b) / CHAR_BIT)
#define BITSET(a, b) ((a)[BITSLOT(b)] |= BITMASK(b))
#define BITCLEAR(a, b) ((a)[BITSLOT(b)] &= ~BITMASK(b))
#define BITTEST(a, b) ((a)[BITSLOT(b)] & BITMASK(b))
#define BITNSLOTS(nb) ((nb + CHAR_BIT - 1) / CHAR_BIT)

    printf("%d\n", CHAR_BIT);
    printf("%d\n",BITMASK(23));
    char bitarray[BITNSLOTS(100)]; /* 容纳100位 */
    printf("%d\n", sizeof(bitarray));
    BITSET(bitarray, 23); /* 把第23位置1 */
    if(BITTEST(bitarray, 35)) /* 判断35位是否为1 */

输入输出

int fscanf ( FILE * stream, const char * format, … )
scanf相当于fscanf(stdin,…)
printf相当于fprintf(stdout,…)
输出到stdout的内容,首先保存到缓冲区;而stderr的,直接输出到屏幕
调用fflush(stdout)将缓冲区内的内容输出到屏幕

    int ch;
/* first */
    ch = getchar();
    putchar('1');
    putchar(ch);
    putchar('\n');
/* second */
    ch = getchar();
    putchar('2');
    putchar(ch);
    putchar('\n');

getchar()从标准输入流得到一个字符(包括空白字符),putchar(char x)输出一个字符到标准输出流,相当于putc(char x, stdout)
空白字符:空格、tab、回车

char str[30]; char c;
gets(str);
puts(str);

c = getchar();
putchar(c);
  1. 键盘输入都被保存在输入缓冲区内,直到用户输入回车,输入函数才会去缓冲区读取。输入函数从缓冲区读取时,如果缓冲区为空,命令行界面会暂停,等待用户输入;否则输入函数会从缓冲区读入对应的数据。

  2. 利用gets 函数读入字符串时,空格和tab 都是字符串的一部分。

  3. gets 函数读入字符串的时候,以回车或EOF 为字符串的终止符,它同时把回车从缓冲区读走,但自己不要回车。fgets则会要。


scanf

int scanf(“格式控制字符串”,参数地址表);
格式控制字符串中通常包含三大类的内容。

  • [空白字符] 以空格和tab 为主。

  • [非空白字符] 除了空白字符和%开头的格式说明符。

  • [格式说明符] 以%开始的格式说明符遵循下面的格式:%[*][width][modifiers]type。可选的星号代表读入的数据将被忽略掉,可选的width 代表最多读入数据的宽度(列数),函数按此宽度从输入数据中截取所需数据;可选的modifiers 主要有h 和l,分别用来输入短整型、长整型和double 类型;type 主要包含c(代表字符),d(代表整数),s(代表字符串)等,当然,还有其他一些类型,大家可以参考本书网站中“扩展内容”网页中的“scanf 的官方定义”链接。

• 参数地址列表是由若干变量的地址组成的列表,与格式说明符相对应,读入的数据根据格式说明符的格式保存到对应的变量中去。

• 返回值代表成功匹配并被读入的变量的个数。如果没有数据被成功读入,函数返回零。

为什么scanf有&

  • C 语言中函数遵循的是单向值传递,我们不能改变传入函数实参的值。但是很明显,scanf 函数必须要改变传入函数实参的值。为了解决这个问题,我们传入变量的地址,函数的形参声明为指针来接受这个地址。数组不用加,因为本身就表示首地址。

当我们调用scanf(“%c”,&c)读取一个字符的时候,输入中的任何字符,包括空白字符,都不会被忽略。
当利用scanf 读入一个整数的时候,情况有些不同,输入中的空格、回车、tab 键会被忽略。
与函数gets 不同,scanf 会忽略输入字符串前面的空格、tab 和回车等空白字符,并且把字符串后面的空白字符当成输入字符串数据的结束。同时,它也不把输入字符串后面的空白字符读入。

scanf 函数按照格式控制字符串给定的格式读取。如果读取失败,scanf 函数会退出,但是它不会从缓冲区内读走不匹配数据。

while((c = getchar()) != '\n' && c != EOF)
; /*`清空缓冲区内多余字符`*/

使用scanf 函数的时候,一个好的习惯就是别忘了判断它的返回值。根据返回值的不同,你就可以判断这个函数是否成功地读入了你所希望的数据;同时,要保证格式控制符和保存输入的变量类型严格一致。
d是整数,c是字符,s是字符串,f是单精度浮点数,hd是short int,ld是long,lf是双精度浮点数。

sscanf

比起scanf 函数,sscanf 函数可以对内存中保存的数据重新进行解析,这就增加了程序的容错性。

    char line[100];
    gets(line);
    int a, b, c;
    char d[10];
    if (sscanf(line, "%d %s %d", &a, d, &b) == 3)
        printf("%d %s %d", a, d, b);
    else if (sscanf(line, "%d %d %d", &a, &b, &c) == 3)
        printf("%d %d %d", a, b, c);
    else
        printf("other form");

printf

与scanf的区别

  • printf 可以用“%f”输出float 和double 两种类型;scanf 必须使用“%f”输入float,“%lf”输入double。

  • 在两个函数的格式控制字符串中,星号“*”有不同的解释。(scanf的*是忽略一个读入数据;printf是从参数获取,如printf(“%*d”, width, num))

  • scanf 格式控制字符串中不使用“\n”。

printf的控制格式中,%m.n代表

  • 浮点数和字符串时,m代表至少要输出m位,如果要输出的小于m位,则用空格填充,否则正常输出。
  • n作用于浮点数时,代表小数点后输出几位;用作字符串时,代表最多输出几个字符。

输入规则小结

1)键盘输入都被保存在输入缓冲区内,直到用户输入回车,输入函数才去缓冲区读取。输入函数从缓冲区读取时,如果缓冲区为空,程序会暂停;否则输入函数会从缓冲区读入对应的数据。

2)getchar 函数每次读任意一个字符(包括回车“”)。

3)利用gets 读入字符串时,空格和tab 都是字符串的一部分。gets 以回车和EOF 为字符串的终止符,不会读入回车,而fgets会读入回车。

4)scanf 中输入数字“%d”或字符串“%s”的时候,scanf 忽略空格、tab、回车等空白字符。scanf 函数利用“%s”提取字符串时,空格、tab 作为字符串输入的截止。

5)如果缓冲区不空,scanf 按自己的格式提取,提取成功,从缓冲区提走数据;提取失败,不从缓冲区提走数据。scanf 函数返回成功提取的数据的个数。

6)掌握利用while 循环清空缓冲区的方法,但是不要用fflush(stdin)。

7)如果你的程序要求对用户输入的各种不规范格式或错误要求有很高的容错程度,尝试一下fgets 和sscanf 的组合来完成用户输入的读取。

安全输入

fgets 函数可以让用户指定输入的最大长度限制,可以用这样来预防溢出攻击。这样,如果你输入超过8 个字符的一个字符串,fgets 只取出前面的7 个字符,然后在后面加上“\0”,保存到“in”这个数组中。fgets 一般是从文件中读取字符串,如果你想让其完成从键盘上输入,可以将标准输入stdin 传入这个函数中。
fgets会读入回车,但因为可以控制长度,不会把数据覆盖到内存的其他地方,就不会产生溢出攻击。所以尽量不要用gets,而用fgets。

函数、模块和宏定义

预处理指令

包括三种,分别为文件包含、条件编译和宏替换。

  • 文件包含即 #include这种,<>表示预处理器从标准库路径开始搜索,“”括起的头文件表示从用户的工作路径开始搜索,所以对于标准头文件,用<>,对于自己的,用“”。
  • 条件编译语句如#if #ifndef #ifdef #endif #undef,适用场景很多,如调试代码中
#ifdef DEBUG
printf("debug information");
#endif

如果是在调试中,加入#define DEBUG即可。
还有可以使得程序支持跨平台编译,有的头文件在不同的平台,可能所在的路径是不一样的。

#ifdef linux
#include
#else
#include
#endif

还可以防止对头文件的重复包含
头文件a.h的内容

#ifndef _A_H
#define _A_H
void funA();
#endif
  • 宏替换,如#define NAME stuff会在预处理阶段,把源码中的NAME替换成stuff,只是简单的替换,不做任何语法和语义方面的检查。主要有两种形式
#define PI 3.14 //object-like
#define PERIMETER(r) (2*PI*(r)) //function-like

C++中提倡尽量避免使用#define。是因为容易出错吗?define可以做到的全局变量和函数也能做到?但全局变量和函数会有编译器帮你纠正一些错误?

static 和const

内存区

静态变量和全局变量都存储在一块叫作静态存储区的内存区内。这个区的值会自动初始化为0。除此之外还有
- 存放字符串常量的常量存储区:当char *p="hello";时,字符串hello就放在常量存储区,p放在栈上
- 代码段,和常量存储区都是只读的
- 堆:malloc分配的内存,使用完毕后必须用free释放,这里的堆不是数据结构的最大堆、最小堆
- 栈:存放函数内局部变量、函数参数、返回数据等,函数结束后自动释放

全局变量和静态变量

全局变量的一个特点是所有函数都能访问它;而静态变量只在定义它的范围内可见,比如定义在一个.c文件中,那么只有这个文件的函数可以读写它。如果用static来修饰函数,就可以在不同的.c文件中定义同名函数。所以static有静态和隐藏两个作用。
如果要定义一个允许被其他.c文件使用的全局变量,则在任一.c文件中定义,不能加static修饰符,在其他.c文件使用这个变量前,用extern修饰符对这个变量进行声明,extern是告诉编译器,这个全局变量已经在某个.c文件中定义了。

编译和链接

编译器只是理解程序,把理解的结果翻译成对应的机器码,并不需要生成可执行文件,而链接器需要在内存中定位这个标识符,需要唯一地找到它。比如我们在某个地方声明了一个函数f,并使用了它,但没有定义(即具体实现),在编译阶段不会报错,到了链接阶段才会报错。
编译器每次只编译一个.c文件,对其他文件一无所知。

声明和定义

对声明的变量或函数,编译器并补申请内存,只是保留一个引用;当执行链接时,把正确的内存地址链接到那个引用上。对定义的变量或函数,编译器要分配一段内存。
可能在其他书上 定义 叫做 实现,声明 叫做 定义,但理解意思就好~

int foo();//声明一个函数
extern int num;//声明一个变量
int i=0;//定义一个变量
int foo(){//定义一个函数
}

头文件

理解这些,就知道头文件是用来放声明的,因为#include语句其实是把当前语句替换成整个头文件的内容。重复定义会造成编译器不知道选哪个定义而随便选一个导致出错(也可能不会随便选,反正别这么做)。
而头文件的避免重复包含只是为了提高编译效率!
所以头文件只应该放这些
- 用于避免重复包含的#ifndef ... #end
- 宏定义
- struct、union、enum等类型的typedef类型定义
- 全局变量及函数的声明(变量前加extern修饰,函数不实现)

库函数

数学相关

大部分在math.h
取整形绝对值的abs()在stdlib.h
三角函数和反三角函数都是以弧度为参数。如求sin45°,应写sin(45*pi/180);

字符串相关

ctype.h里有字符类型判断的,如isalnum, islower, isupper, issspace
string.h里有strcpy, strcat, strlen, strcmp。strcpy, strcat函数会改变传入的字符串的内容,需要确保传入的地址有足够的空间容纳改变后的字符串,也需要确保源地址和目标地址不会发生重叠,这两个函数对传入的空间不做容量检查。

    char str[11]="hello";
    char str1[11]="world";
    strcat(str1,str);
    printf("%s\n",str1);

    strcpy(str,str1);
    printf("%s\n",str);

为了解决字符串函数的溢出问题,引入了strncpy和strncat函数,允许指定复制或追加的字符的个数。

    char str[11]="hello";
    char str1[11]="world";
    strncat(str,str1,3);
    printf("%s\n",str);
    strncpy(str1,str,6);
    printf("%s\n",str1);

strlen源代码

size_t strlen(const char *string) {
    const char *eos;
    while (*eos++) {
        return (eos - string - 1);
    }
}

strcpy源代码

char *strcpy(char *dst, const char *src) {
    char *cp = dst;
    while (*cp++ = *src++);
    return (dst);
}

字符和数字相互转换

字符串转数字

    double d;
    int i;
    long l;
    char num1[] = "123";
    char num2[] = "1.001";
    char num3[] = "1234567890";
    d = atof(num2);
    i = atoi(num1);
    l = atol(num3);
    printf("%lf %d %ld", d, i, l);

数字转字符串

    double d = 123.45;
    int i = 123;
    long l = 1234567890;
    char num1[20];
    char num2[20];
    char num3[20];
    sprintf(num1, "%lf", d);
    sprintf(num2, "%d", i);
    sprintf(num3, "%ld", l);

时间函数

系统时间为 从1970年1月1日0时0分0秒到这一刻所经过的秒数,可调用time_t time(time_t *timer)获取,time_t是long,通过typedef定义的。有两种获得系统时间的方法

time_t now;
time(&now);
now=time(NULL);

随机数

扑克洗牌

void wash(int *pai) {
    srand(time(NULL));
    int i;
    for (i = 0; i < 54; i++) {
        int c = rand() % 54;
        int t = pai[i];
        pai[i] = pai[c];
        pai[c] = t;
    }
}

产生100个0到1的随机数

void produce100(double *arr) {
    srand(time(NULL));
    int i, randN;
    for (i = 0; i < 100; i++) {
        randN = rand();
        arr[i] = (double) randN / RAND_MAX;
    }
}

系统相关函数

exit

与return相比,有几个优点

  1. 在程序任何一个地方调用都可以终止当前程序,而return只有在main调用才能终止
  2. 当exit函数被调用时,会执行一些额外操作,如刷新所有用于流的缓冲区,关闭打开的文件,还可以通过atexit函数注册一些退出函数。

system

把传入的字符串参数传送给宿主操作系统。如在Linux下,system("ls"); 就是列出当前目录文件

signal

在程序收到指定信号时,指定你要调用的回调函数。
使用任何库函数,一定要包含与之对应的正确的头文件,否则可能发生难以发现的逻辑错误

数组

数组变量就是此数组的首地址,如int array[10]; array等价于&array[0]。
定义一个数组变量的时候,编译器会根据数组的长度声明一块连续的内存给数组。
数组本身不会运行越界检查。(C语言信任程序)

数组的初始化与数组间的赋值

    int arr[3];//随机值
    int arr1[3]={1};//当指定第一个元素后,后面的全为零
    int arr2[3]={1,2,3};
    memset(arr,0, sizeof(arr));//这样是把arr这块内存每个字节设为0,而不是把arr每个元素值设为0

    memcpy(arr,arr2, sizeof(arr2));//数组间赋值的高效方法

数组与函数

向一个函数传递一个数组,其实是把数组的首地址传入。因为如果采用值传递,我们需要在栈上再构造一个数组,然后把数据复制到新的数组,这样效率很低。所以用的是引用传递,传递的是地址值。(这里的引用传递不同于Java的,这里其实还是传值,不过传的是地址值)
若不希望函数能改变参数中数组的值,可以把形参声明为const int arr[]
C语言的函数不支持用return返回一个数组。可以用全局变量,返回指针,改变实参这些代替。

二维数组

因为C语言不检查数组越界,所以这里arr[0][4]arr[1][1]指向的是同一个元素

    char arr[2][3]={'a'};
    printf("%c ",arr[0][0]);
    arr[0][4]='z';
    printf("%c",arr[1][1]);

定义一个数组的时候,必须指定数组的长度,或者通过初始化每个元素的方法来隐含指定长度

指针

这一章之前写过了,在这里

http://blog.csdn.net/zhaobudaofangxia/article/details/53333489

以下是重读的笔记
任何指针类型变量都有两个属性:本身保存的地址和指向变量的类型。
指针本身只存储地址,编译器根据指针指向的变量的类型从指针保存的地址向后寻址。
野指针:当定义一个指针没有初始化的时候,那么它指向的就是一个不确定的地址。
定义一个指针类型的变量时,暂时不知道指向哪就让它指向NULL。

void

对于void类型指针,它只保存一个地址,不包含指向变量的类型信息,所以对其进行算术运算和进行取值操作都是不允许的。
void类型指针一般用在函数的参数和返回值中,以达到泛型的目的。如memcpy和memset,操作的对象仅仅是一片内存,不关心这片内存保存的是什么类型。

指针和数组

当a[i]用在一个表达式中的时候,编译器自动将其转换为指针加偏移量*(a+i)的形式。
当数组类型被用于声明函数形参的时候,编译器会自动将其转换为指针。
但编译器并不是一看到数组a[i]就将其转化为指针,如sizeof(a)的时候,返回的是整个数组的长度,而不是指针长度。

指针数组

    char *prompt[]={"1.语文","2.MATH","3.English",NULL};
    char **temp;
    temp=prompt;
    while(*temp){
        printf("%s\n",*temp);
        temp++;
    }

使用指针数组的一个优点就在于对多个字符串的排序,排序的过程并不需要移动真实的字符串,只需要改变指针的指向。

动态内存分配

malloc和calloc

    char *str=malloc(sizeof(char)*10);
    memset(str,1, sizeof(char)*10);
    str=calloc(10,sizeof(char));

calloc会将所分配的内存空间中的每一位都初始化为零。
使用calloc比调用malloc加memset要块。但calloc在不同平台好像有点差异,所以最好别用?
使用这两个函数记得包含头件

realloc

void *realloc(void *ptr, size_t size)

  • 第一个参数所指向的指针,或者是NULL,或者是malloc、calloc、realloc三个函数返回的指针。
  • 返回值一般用一个临时指针变量保存,因为一旦realloc调用失败,将返回NULL,所以判断非NULL再赋值

free

即使在函数内部,用malloc申请的内存也不会伴随着函数的退出而释放,必须用free来显示释放。否则内存泄漏,内存被逐步占用而不释放,电脑会越来越慢(分配新的内存时找得慢?),直到死机。

  • free后的内存并不返还给操作系统,而是对同一程序中未来的malloc函数的调用可用。
  • free函数中的指针必须是malloc、calloc、realloc函数返回的指针
  • free函数并不能改变传入的指针的值
    而使用被释放的内存或者重复释放内存都会发生错误。
    所以当free完后同时把这个指针指向NULL,就在就不会 使用或重复释放 已经释放的内存了。

内存操作函数

memmove函数可以用在源内存和目标内存去重叠的情况,而memcpy不可以。memmove从最后开始复制来解决这个问题,而不是从头开始复制。

内存使用的效率建议

反复调用malloc和free,堆将变得很“碎”,这样malloc的效率将很低,因为需要花费很长时间来找到 一块合适的地方。所以如果需要频繁使用多个较少的内存空间,更好的办法是预先分配一块大内存。这就是资源池中的“内存池”了

动态数组

C语言中的数组在内存中连续分布的。所以可以利用calloc或者malloc来申请一块用来容纳动态数组的连续的内存。(C++里直接用vector就好了)

字符串

字符串以一个“\0”字符为截止符。申请char数组时要预留一个截止符位置。

char *cp="const";//const在常量存储区,cp在静态存储区
char ca[]="static";//static和ca在静态存储区
int main() {
    char *p="const1";//const1在常量存储区,p在栈上
    char a[]="stack";//都在栈上
    return 0;
}

函数和指针

void f(int *ptr){
    ptr=(int *)malloc(sizeof(int));
    *ptr=999;
    //内存泄漏
}
int main() {
    int *p,k=5;
    p=&k;
    f(p);
    printf("%d",*p);
    return 0;
}

这里输出的是5,所以其实还是值传递的,ptr和p不是同一个地址值,只是值相同,所以只是改变了ptr的指向,没有改变原本ptr指向的内存的值。
如果单纯想改变*p的值,直接*p=123;即可。如果申请了内存,可通过返回指向该内存的指针 (如下)

int *f(int *ptr){
    ptr=(int *)malloc(sizeof(int));
    *ptr=999;
    return ptr;
}
int main() {
    int *p,k=5;
    p=f(p);
    printf("%d",*p);
    free(p);
    return 0;
}

或 用指向指针类型的指针做到。(如下)

void f(int **ptr){
    *ptr=(int *)malloc(sizeof(int));
    **ptr=999;
}
int main() {
    int *p,k=5;
    p=&k;
    f(&p);
    printf("%d",*p);
    free(p);
    return 0;
}

函数指针

函数指针可以达到“泛型”的一种概念。
C语言用一个函数指针定义一个接口规范,而C++用多个函数指针来定义。(虚基类维护的一个虚函数表里保存着类中所有对应虚函数的函数指针)

复杂声明

void (*ap[10])(void (*)());
//一个长度为10的指针数组,指针是函数指针,设为a,
// a指向一个返回值为void,参数为一个函数指针(设为b)的函数,
// b指向一个返回值为void,没有形参的函数

int (*frp(int))(char *, char *);
//这是一个函数,函数本身接受一个int形参,返回值是一个函数指针,
// 指向的函数返回一个int,接受两个字符指针作为形参

void (*fp)();
//这是一个函数指针,指向一个返回值为void,没有形参的函数。

第二个函数可以这么用frp(1)("hello", "world"); 因为frp的返回值是一个函数。而frp要这样定义

int CE(char *c, char *e){
    return 1;
}
int (*frp(int))(char *, char *){
    return CE;
}

用typedef定义一个复杂声明

void (*ap[10])(void (*)());

typedef void (*pfv)();
//定义了一个函数指针,指向一个返回值为void,没有形参的函数
typedef void (*pf_taking_pfv)(pfv);
//定义了一个函数指针,参数为pfv
pf_taking_pfv ap[10];
//定义了一个指针数组,指针为pf_taking_pfv
void (*f1())();

typedef void (*pfv1)();
pfv1 f1();

结构体

实际上,struct就是所有成员都是public的一个class。
定义结构体变量(用这种方法最好)

typedef struct {
    int id;
    char name[20];
} STUD;
STUD student1;

还有其他方法比如

struct student{
    int id;
    char name[20];
} student1;
struct student student2;
struct {
    int id;
    char name[20];
} student1;

内存对齐的目的是使处理器能够更快速的进行寻址。
两个相同类型的结构体变量不能直接用等号来进行赋值,也不能直接用==来判断是否相等(C++中可以通过重载做到)。正确的方法是编写一个函数来做这个工作(参数用结构体类型的指针,不然拷贝一次效率低)。

枚举

枚举类型是一种基本数据类型。

typedef enum DAY {
    MON = 1, TUE, WED
}Day;

int main() {
    Day today;
    today=MON;
    printf("%d",today);
    today=2;
    printf("%d",today);
    return 0;
}

书上说枚举变量只能用枚举符赋值,但好像在C11里面已经可以用任意整形赋值?

文件

FILE是C语言中定义的一个结构类型,定义在stdio.h中。
FILE *就是人们常说的文件句柄了。

r+, w+

r代表读,w代表写,+代表读写。如果文件不存在,r+就失败,而w+会新建文件;如果文件存在,r+不清空文件,而w+清空文件。
同时读写,常见的做法是使用一个临时文件专门用来写,写完之后在替换。因为如果对一个文件同时读写,写的时候一旦出错,会破坏原文件内容。

断行标识符

在电传打字机时代,每秒钟可以打10个字符,但是换行要0.2秒,要是这段时间有新的字符传过来,那新传过来的就丢失了。于是工程师们在每行后面加两个表示结束的字符。一个叫回车(\r),告诉打字机把打印头定位在左边界。另一个叫换行(\n),告诉打字机把纸向下移一行。
后来计算法出现,这两个概念也被搬到计算机上。但一些程序员认为两个字符表示结尾太浪费了,于是出现分歧。

  • UNIX/Linux下:用换行表示
  • DOS/Windows下:用回车+换行表示
  • Mac下:用回车表示

而C语言的I/O补直接读写硬盘,而是通过调用OS的API完成,所以我们可以统一用\n来表示换行。

文本格式和二进制格式

打开模式中增加字母‘b’即可以二进制模式打开文件。
当用fwrite函数时,一旦字节流发现0x0A,就会转换为0x0D 0x0A。
要想不转换,应该以二进制模式b打开文件。

feof

feof返回真,则文件位置指针一定指向末位的EOF,反过来不成立。
feof内部是通过判断一个标志位来返回真的。而这个标志位当fgetc、fgets、fscanf、fread读取到了EOF的时候就会设置。

int main() {
    FILE *fp=fopen("test","r");
    if(fp==NULL)
        printf("can not open file\n");
    char str[100];
    while(!feof(fp)){
        fgets(str,100,fp);
        printf("%s",str);
    }
    return 0;
}

几乎所有的读函数,当读取到末位或读取出错的时候,都会返回相同的值,所以要分别使用feof和ferror来判断哪一种情况发生。

int c;
while(c = fgetc(fp)){
    if(c==EOF){
        if(feof(fp)!=0)
        break;
        if(ferror(fp)!=0)
        error_handle;
    }else{
        do_something;
    }
}

异常

异常底层的实现借助于栈回退技术。

tips

  1. 用一个int变量来控制循环,如for(...;...&&flag;...),这样可以方便的跳出多重循环

END

2017/5/17 之后继续学Linux和csapp,有时间就继续看C++ Primer

你可能感兴趣的:(C指针,C语言,读书笔记)