C语言知识点总结

537 阅读28分钟

1. 大小端

参考博客: (1)blog.csdn.net/qq_39412582… (2)baike.baidu.com/item/%E5%A4… (3)blog.csdn.net/qq_29350001…

大端存储:就是把一个数的低位字节序的内容存放到高地址处,高位字节序的内容存放在低地址处。

小端存储:就是把一个数的低位字节序的内容存放到低地址处,高位字节序的内容存放在高地址处。

从定义可以看出,大端存储与我们人眼的习惯相符合。 速记:低字节序在哪就是哪。

1.1 怎么检测设备是是按大端存储还是小端存储

#include <stdio.h>
int main()
{
    short int x;
    char x0;
    x=0x1122;
    x0=((char*)&x)[0]; // 低地址单元
    if (x0 == 0x11) {
        printf("big endian");
    } else if (x0 == 0x22) {
        printf("little endian");
    }
    return 0;
}

作者在window上跑这段程序是小端存储。由于没有大端设备,所以这里没法验证大端的情况。(我找了在线运行的网站,都是小端设备。如果有哪位小伙伴知道大端在线运行的网站,请一定给我留言。) 但是这里其实要和截断区别,截断都是截的低位字节的,与是低地址还是高地址无关。 比如,下面这段程序,由于低位字节是0x78,所以它的运行结果不管是在大端设备还是小端设备,都应该是输出0x78。(没有设备验证)

#include <stdio.h>
int main()
{
    char x1;
    int x2 = 0x12345678;
    x1 = x2;
    printf("\n%x", x1);
    return 0;
}

1.2 如何进行大小端的转换

1.2.1 方法一:位操作


#include<stdio.h>  
typedef unsigned int uint_32 ;  
typedef unsigned short uint_16 ;  
 
//16位
#define BSWAP_16(x) \
    (uint_16)((((uint_16)(x) & 0x00ff) << 8) | \
              (((uint_16)(x) & 0xff00) >> 8) \
             )
             
//32位               
#define BSWAP_32(x) \
    (uint_32)((((uint_32)(x) & 0xff000000) >> 24) | \
              (((uint_32)(x) & 0x00ff0000) >> 8) | \
              (((uint_32)(x) & 0x0000ff00) << 8) | \
              (((uint_32)(x) & 0x000000ff) << 24) \
             ) 

int main()  
{
    printf("%#x\n",BSWAP_16(0x1234));  
    printf("%#x\n",BSWAP_32(0x12345678));  
    return 0 ;  
}

1.2.2 方法二:使用 htonl, htons, ntohl, ntohs 等函数

htonl()     //32位无符号整型的主机字节顺序到网络字节顺序的转换(小端->>大端)
htons()     //16位无符号短整型的主机字节顺序到网络字节顺序的转换  (小端->>大端)
ntohl()     //32位无符号整型的网络字节顺序到主机字节顺序的转换  (大端->>小端)
ntohs()     //16位无符号短整型的网络字节顺序到主机字节顺序的转换  (大端->>小端)

注,主机字节顺序,X86一般多为小端(little-endian),网络字节顺序,即大端(big-endian);

#include <stdio.h>
#include <arpa/inet.h>
int main ()
{
    union
    {
            short i;
            char a[2];
    }u;
    u.a[0] = 0x11;
    u.a[1] = 0x22;
    printf ("0x%x\n", u.i);  //0x2211 为小端  0x1122 为大端
    printf ("0x%.x\n", htons (u.i)); //大小端转换 
    return 0;
}

输出:

0x2211
0x1122

2 内联(inline)

以 inline 修饰的函数叫内联函数。内联函数在调用时不是像一般函数那样要转去执行被调用的函数体,执行完后再转回调用函数中,执行之后的语句;而是在调用处用内联函数体的代码来替换,这样没有函数压栈,将会节省调用的开销,提高运行效率。 内联函数必须是和函数体定义在一起才有效。

内联函数和宏的区别:

1. 内联函数在运行时可调试,而宏定义不可以
2. 编译时会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义不会

何时使用内联函数: 建议当函数在 10 行以内时才将其定义为内联函数。

以下情况不宜使用内联:

(1) 如果函数体内的代码比较长、使用内联将导致内存消耗代价较高
(2) 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

3 数据类型转换

参考博客: blog.csdn.net/zhangzhi123… 一个原则:

若要扩展量为有符号量,不管扩展成有符号还是无符号,都遵循符号扩展;若要扩展量为无符号量,不管扩展成有符号还是无符号,都遵循零扩展。

char a = 0xff; // a为-1,其为有符号量,二进制为11111111
unsined short b = a; // 此处a要进行符号扩展, b的二进制为1111111111111111
unsigned char a = 0xff; // a为无符号量,二进制为11111111
short b = a; // 此处a要进行零扩展,b的二进制为00000000 11111111

总结:

(1) 短数据类型扩展为长数据类型: 要扩展的短数据类型为有符号数的,进行符号扩展。要扩展的短数据类型为无符号数,进行零扩展。

(2) 长数据类型缩减为短数据类型: 如果长数据类型的高字节全为1或全为0,则会直接截取低字节赋给短数据类型;如果长数据类型的高字节不全为1或不全为0,则转会就会发生错误。

(3) 同一长度的数据类型中有符号数与无符号数的相互转化: 直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另短类型扩展为长类型时,但短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。

4. 字节对齐

  1. struct/union 空间大小计算原则:
  • 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  • 结构体每个成员相对于结构体首地址的偏移量都是成员自身大小的整数倍,如有需要编译器会在成员之间加上填充字节;
  • 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
  1. 字节对齐的定义:
  • 自然对齐(数据类型的长度)
    (1)对于 char 型数据,自然对齐的长度为 1 字节,short 为 2 字节,int 为 4 字节,float 为 4 字节,当然这些数据类型的对齐长度可能和编译器有关。
    (2)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  • 指定对齐: #progma pack(n)中 n 的值,如果没有指定,则为 CPU 默认对齐值,32 位默认 4 字节对齐,64 位默认 8 字节对齐。
  • 有效对齐: 自然对齐和指定对齐的最小值

结构体对齐步骤
(1) 每个成员变量分别按自己的"有效对齐"作对齐
(2) 取所有成员变量的"有效对齐"的最大值,作为结构体的"有效对齐"
(3) 结构体成员及结构体自己的起始地址和长度均为其“有效对齐”的整数倍

#include <stdio.h>

struct tagA {
    char a;
    short b;
    char d;
};

struct tagB
{
    char a;
    int b;
    char d;
};

struct tagC
{
    char a;
    short b;
    int c;
    char d;
};

int main()
{
    struct tagA a, b[2];
    printf("struct tagA :sizeof(a):%d, sizeof(b):%d\n", sizeof(a), sizeof(b));

    struct tagB c, d[2];
    printf("struct tagB :sizeof(c):%d, sizeof(d):%d\n", sizeof(c), sizeof(d));

    struct tagC e, f[2];
    printf("struct tagC :sizeof(e):%d, sizeof(f):%d\n", sizeof(e), sizeof(f));
    return 0;
}

按照结构体对齐步骤分析:首先分析结构体 tagA,它的成员的有效对齐是 a 是 1,b 是 2,d 是 1。所以结构体 tagA 的有效对齐是 2。最后,tagA 的成员 a 的起始地址是 1 的整数倍即可,b 的起始地址是 2 的整数倍即可,d 的起始地址是 1 的整数倍。结构体 tagA 的总长度是 2 的整数倍。所以结构体 tagA 的长度是 6.所以 stb[2]的长度是 12. 其他的类似分析。 运行结果:

struct tagA :sizeof(a):6, sizeof(b):12
struct tagB :sizeof(c):12, sizeof(d):24
struct tagC :sizeof(e):12, sizeof(f):24

5 预处理器

5.1 宏

#define name stuff

每当有符号 name 出现在这条指令后面时,预处理器就会把它替换成 stuff。注意,后面不要带分号。如果定义中的 stuff 非常长,它可以分成几行,除了最后一行外,每行的末尾都要加一个反斜杠,例如:

#define DEBUG_PRINT printf( "File %s line %d:" \
"x=%d, y=%d, z=%d", \
__FILE__, __LINE__, \
x, y, z)
  • 带参数的宏
#define name(parameter-list) stuff

例如:

#define SQUARE(x) x * x

所以,SQUARE(5),预处理器就会替换成 5 * 5

但是如果:

a = 5;
printf("%d\n", SQUARE(a + 1));

本意以为会输出 36。但是实际上我们将用到的宏的地方替换,其实是

printf("%d\n", a+1 * a + 1);

即输出的是 11。 为了解决这个问题,只要在宏参数中加上两个括号就行:

#define SQUARE(x) (x) * (x)

但是现在有另一个宏

#define DOUBLE(x) (x) + (x)

又会出现另一个问题。例如:

a = 5;
printf("%d\n", 10 * DOUBLE(a));

我们期望的值是 100。但是通过宏展开得到:

printf("%d\n", 10 *(x) + (x));

得到的结果是 55。 这个错误也很容易修正:只要在整个表达式两边加上一对括号即可:

#define DOUBLE(x) ((x) + (x))

所以,一般在定义宏时,首先每个宏参数都加上括号,其次整体表达式也要加上一对括号。

  • 宏与函数 我们经常用宏来执行一些简单地计算,比如选出两个数中较大的一个:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

为啥不用函数呢? 1、用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。 2、更重要的是,函数的参数必须声明为一种特殊的类型,所以它只能在类型合适的表达式上使用。但是宏是与类型无关的。例如,上面这个宏可以用于整型、长整型、单浮点型、双浮点型等。 但是和函数比,宏的缺点是每次使用宏时,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅增加程序的长度。一般建议宏定义不要超过 10 行。另外,有些任务是函数完成不了的。 例如:

#define MALLOC(n, type) \
( (type *)malloc((n) * sizeof(type)))
pi = MALLOC(25, int);

将被替换成:

pi = ( (int *)malloc((25) * sizeof(int)));
  • 不要使用带副作用的宏参数 当宏参数在宏定义中出现的次数超过一次时,如果这个参数有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。 例如:
#define MAX(a, b) ( (a) > (b) ? (a) : (b))
....
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d, y=%d, z= %d\n", x, y, z);

第一个表达式是条件表达式,用于确定执行两个表达式中的哪一个,剩余的那个表达式将不会执行。 那上面这段代码的输出是多少呢? x =6, y=10, z= 9; 为什么呢? 我们将宏定义展开:

z = ( (x++) > ( y++) ? (x++) : (y++));

首先是比较两个表达式,比较完后,x= 6, y = 9.并且由于 y 比 x 大,所以在比较完后 y 还会再执行一次 y++。所以最终的结果是 y =10。

  • #undef
#undef name

这条预处理指令由于移除一个宏定义。 如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用。#undef 移除。

  • 预处理器运算符

    (1) 字符串常量化运算符(#):在宏定义中,当需要把一个宏的参数转换成字符串常量时,可以使用字符串常量运算符(#):

#include <stdio.h>
#define P(A) printf("%s:%d\n", #A, A)
int main() {
    int a = 1;
    P(a);
    return 0;
}

输出:

a:1

(2)标记粘贴运算符(##):宏定义内的标记粘贴运算符(##)会合并两个参数。

#include <stdio.h>
#define P(A, B) printf("%d##%d = %d", A, B, A##B)
int main() {
    P(5, 6);
    return 0;
}

输出:

5##6 = 56

注意:当宏参数是另一个宏的时候,需要注意的是凡宏定义中有用#或者##的地方宏参数时不会再展开:

#include <stdio.h>
#define f(x, y) x##y
#define g(x) #x
#define h(x) g(x)

int main()
{
    printf("%s, %s\n", g(f(1, 2)), h(f(1,2)));
    return 0;
}

解析:第一个表达式 g(f(1,2)),g(x)的定义中有#,不展开 f(x,y)的宏,直接替换成#f(1,2),打印输出为 f(1,2)。 第二个表达式 h(f(1, 2)),h(x)的定义中没有#或者##,需要展开 f(x, y)的宏, 即 1##2,即 h(12)->g(12), 最终结果是 12。

  • 预定义宏
含义
FILE进行编译的源文件名
LINE文件当前行的行号
DATE文件被编译的日期
TIME文件被编译的时间
STDC如果编译器遵循 ANSI C,其值为 1,否则未定义
  • 预处理器和宏定义——typedef 和 define typedef 与 define 都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在以下几个不同: (1) 原理不同。#define 是 C 语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,不管含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,它在编译时出来,所以 typedef 有类型检查的功能。 (2) 功能不同。typedef 是用来定义类型的别名,这些类型不只包含内部类型(int 、char 等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。例如定义一个函数指针。#define 不只是可以为类型取别名,还可以定义常量、变量、编译开关等。 (3) 作用域不同。#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。

  • 预处理器与宏定义——define 和 const

    define 与 const 都能定义常量,效果虽然一样,但是各有侧重。define 既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错。而 const 的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下几个方面:

(1) define 只是用来进行单纯的文本替换,define 常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中它只是一个常数,一个命令中的参数并没有实际的存在;而 const 常量存在于程序的数据段,并在堆栈中分配了空间,const 常量在程序中确确实实的存在,并且可以被调用、传递。

(2) const 常量有数据类型,而 define 常量没有数据类型。编译器可以对 const 常量进行类型安全检查,如类型、语句结构等,而 define 不行。

5.2 条件编译

#if constant-expression
 statements
#endif

#if constant-expression
 statements
#elif constant-expression
 other statements
#else
 other statements
#endif

是否被定义:

#if  defined(symbol)
#ifdef symbol

#if !defined(sysmbol)
#ifndef symbol

5.3 文件包含

#include 指令使另一个文件的内容被编译,就像它实际出现于#include指令出现的位置一样。这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。所以,如果一个头文件被包含到 10 个源文件中了,它实际会被编译 10 次。但是并不会对运行时效率有影响。

#include <filename>
#include "filename"
  • 嵌套文件包含

嵌套文件包含的一个不利之处在于一个头文件可能会被多次包含,比如:

#include "a.h"
#include "b.h"

如果 a.h 和 b.h 都包含一个嵌套的#include 文件 x.h,那么 x.h 在此处会出现两次。 为了解决这个问题,一般头文件这样写:

#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
......
#endif

当头文件第一次被包含时,它被正常处理,符号_HEADERNAME 被定义成 1.如果头文件再次被包含,通过条件编译,它的所有内容被忽略。 上面定义也可以写成这样:

#define _HEADERNAME_H

6 指针

请见另一篇文章C语言指针总结

7 变量存储

BSS段(bss segment):通常是指用来存放程序中未初始化的全局变量(包括静态变量)和初始化为 0 的的全局变量(包括静态变量)的一块区域。BSS 是英文 Block Started By Symbol 的简称。BSS 属于静态内存分配。
Data 段(data segment):数据段,用来存放已经初始化且初始化值为非零的全局变量(包括静态变量),数据段属于静态内存分配。
代码段(code segment/text segment):通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
字符串常量(rodata):该区域存放的是字符常量,属于只读区域,有些教材把这个区域归位于代码段。
堆(heap):堆是用来存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或收缩。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被踢除(堆被缩减)
栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量(不包括静态局部变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中)

#include <stdio.h>
int bss_array[1024 * 1024] = {0};
int main()
{
    return 0;
}

变量 bss_array 的大小为 4 *1024*1024 = 4M。但是其可执行文件的大小却远没有 4M。说明 bss 类型的全局变量只占运行时的内存空间,而不占文件空间。 但是把上面的数组赋值为非零时:

#include <stdio.h>
int data_array[1024 * 1024] = {1};
int main()
{
    return 0;
}

文件大小就变为 4M 多了。由此可见,data 类型的全局变量是既占文件空间,又占运行时内存空间的。

8 static 关键字

对全局变量增加 static 修饰,将导致该变量的作用域发生变化,该变量只在该文件中生效。 (全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并没有不同) 对局部变量增加 static 修饰,将导致该变量的存储区域发生改变,该变量从栈变为静态区域(可能是 bss 段,也可能是 data 段) 初始化:static 局部变量只被初始化一次,下一次依据上一次的结果值。 存储方式:普通局部变量存放在栈中,而 static 局部变量存放在静态区。 对于如下程序:

int c;
void func(int a)
{
    static int b;
    int array[5] = {0};
    int *p = (int *)malloc(100);
    b = a;
    c = b + 1;
    return;
}

以下说法正确的是: A:放在栈中的变量有 a, p, array; B:放在栈中的变量有 a, array, b; C:放在 BSS 段里的变量有 b,c D:放在堆中的是 p 指向的内存

答案:ACD

9 Cache 存储器与二维数组

cpu 的缓存机制:cpu 从内存中读取数据的时候,会先把读取的数据加载到 cpu 的缓存中。而 cpu 每次从内存中读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块(一段连续的内存地址)并保存到 cpu 的缓存中,然后下次访问内存数据的时候就会先从 cpu 缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,这同时也是 cpu 缓存存在的意义:为了弥补内存访问速度过慢与 cpu 执行速度快之间的差异而引入的。 由于数组是行优先的。所以访问同样一个二维数组。你先访问行还是先访问列,其实性能上是有区别的。 看下面的一段代码:

#include <stdio.h>
#include <time.h>

#define MAX_SIZE 12000
int a[MAX_SIZE][MAX_SIZE];
void func1()
{
    clock_t startTime;
    clock_t endTime;
    startTime = clock();
    for (int i = 0; i < MAX_SIZE; i++) {
        for (int j = 0; j < MAX_SIZE; j++) {
            a[i][j] = i + j;
        }
    }
    endTime = clock();
    double total_time = (double) (endTime - startTime) / CLOCKS_PER_SEC;
    printf("func1: %f seconds\n", total_time);
    return;
}

void func2()
{
    clock_t startTime;
    clock_t endTime;
    startTime = clock();
    for (int j = 0; j < MAX_SIZE; j++) {
        for (int i = 0; i < MAX_SIZE; i++) {
            a[i][j] = i + j;
        }
    }
    endTime = clock();
    double total_time = (double) (endTime - startTime) / CLOCKS_PER_SEC;
    printf("func2: %f seconds\n", total_time);
    return;
}

int main() {
    func1();
    func2();
    return 0;
}

func1 和 func2 都是对二维数组 a 赋值。只是它们访问的顺序不同:func1 先访问行,再访问列。func2 是先访问列,再访问行。 但是它们的运行时间却有区别:从下面的结果可以看到,func1 的执行时间比 func2 的少。 造成这个差异的原因就是上面所说的的 cpu 缓存机制。由于数组是行优先,所以对于 func1。当它访问元素 a[0][0]时,由于下面要访问的是它的地址 a[0][1]、a[0][2]......。这些地址会命中 cache。所以就直接从 cache 中读取了,而不必执行访问内存的操作了。但是对于 func2。当它读 a[0][0]时,接下来读取的是 a[1][0]、a[2][0]。这些地址都不在这个 cache 中,所以接下来每访问一个元素,它就必须执行一次访问内存的这个操作,进而 func2 的性能肯定会比 func1 的差。

10 C 语言常用的字符串函数总结

参考博客: www.cnblogs.com/LydiammZuo/…

  1. strlen
  • 原型:size_t strlen(char const* string);

  • 功能:返回字符串 string 的长度(不包含字符串终止符 NUL)

  • 注意:size_t 是一个无符号整数类型

  • 举例:

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

int main()
{
    char* y = "abcdef";
    char* x = "abcd";

    if (strlen(x) >= strlen(y)) {
        printf("1: strlen(x) >= strlen(y)\n");
    } else {
        printf("1: strlen(x) < strlen(y)\n");
    }

    /* 由于strlen(x)返回的是一个size_t,所以strlen(x) - strlen(y) >= 0恒成立,
     * 导致出现错误的结论
     */
    if (strlen(x) - strlen(y) >= 0) {
        printf("2: strlen(x) - strlen(y) >= 0\n");
    } else {
        printf("2: strlen(x) - strlen(y) < 0\n");
    }

    // 将size_t转换为int类型后,可以返回正确的值
    if ((int)(strlen(x)) - (int)(strlen(y)) >= 0) {
        printf("3: (int)strlen(x) - strlen(y) >= 0\n");
    } else {
        printf("3: (int)strlen(x) - strlen(y) < 0\n");
    }

    return 0;
}

运行结果:

1.png

  1. strcpy
  • 原型:char *strcpy(char *dst, char const *src);

  • 功能:将参数 src 字符串复制到 dst 参数中。如果参数 src 和 dst 在内存中出现重叠,其结果是未定义的。由于 dst 参数将进行修改,所以它必须是个字符数组或者是一个指向动态分配内存的数组的指针,不能使用字符串常量。返回参数 dst 的一份拷贝。

  • 注意:目标参数 dst 的以前内容将被覆盖并丢失。即使新的字符串比 dst 原先的内存更短,由于新字符串是以 NUL 字符结尾,所以老字符串最后剩余的几个字符也会被有效的删除。如果字符串比数组长,多余的字符仍被复制,它们将覆盖原先存储于数组后面的内存空间的值。所以必须保证目标字符数组的空间足以容纳需要复制的字符串。

  • 举例:

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

int main()
{
    char msg[] = "Original message";
    printf("before strcpy: msg:%s\n", msg);
    strcpy(msg, "Different");
    printf("after strcpy: msg:%s\n", msg);
    return 0;
}

运行结果:

2.png

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

int main()
{
    char msg[] = "Different";
    printf("before strcpy: msg:%s\n", msg);
    strcpy(msg, "Original message");
    printf("after strcpy: msg:%s\n",msg);
    return 0;
}

3.png

  1. strncpy
  • 原型:char *strncpy(char *dst, char const *src, size_t len);

  • 功能:和 strcpy 一样,strncpy 把源字符串的字符复制到目标数组。然而,它总是 正好向 dst 写入 len 个字符。如果 strlen(src)的值小于 len, dst 数组就用额外的 NUL 字节填充到 len 长度。如果 strlen(src)的值大于或者等于 len,那么只有 len 个字符被复制到 dst 中。

  • 注意:strncpy 调用的结果可能不是一个字符串,它的结果将不会以 NUL 字符结尾, 因此字符串必须以 NUL 字符结尾。

  • 举例

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

#define STR_CPY_LEN 5
int main()
{
    char msg[] = "Original message";
    printf("before strncpy: msg:%s\n", msg);
    strncpy(msg, "Different", STR_CPY_LEN);
    printf("after strncpy: msg:%s\n",msg);
    return 0;
}

运行结果:

4.png

  1. strcat
  • 原型:char *strcat(char *dst, char const *src);

  • 功能:将一个字符串添加(连接)到另一个字符串的后面。

  • 注意:src 和 dst 的所指的内存区域不能重叠,如果发生重叠,其结果是未定义的。

  • 举例:

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

int main()
{
    char msg[20] = "hello";
    printf("before strcat: msg:%s\n", msg);
    strcat(msg, ", world.");
    printf("after strcat: msg:%s\n", msg);
    return 0;
}

运行结果:

5.png

  1. strncat
  • 原型:char *strncat(char *dst, char const *src, size_t len);

  • 功能:它从 src 最多复制 len 个字符到 dst 中。但是, strncat 总是在结果字符串后面添加一个 NUL 字符。

  • 注意:src 和 dst 所指的内存区域不能重叠,并且 dst 必须有足够多的空间来容纳 src 的字符串。

  • 举例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LEN 5
int main()
{
    char msg[20] = "hello";
    printf("before strcat: len: %d, msg:%s\n", strlen(msg), msg);
    strncat(msg, ", world.", LEN);
    printf("after strcat: len: %d, msg:%s\n", strlen(msg), msg);
    return 0;
}

运行结果:

6.png

  1. strcmp
  • 原型:int strcmp(char const *s1, char const *s2);

  • 功能:比较两个字符串。如果 s1 小于 s2,strcmp 函数返回一个小于零的值。如果 s1 大于 s2,函数返回一个大于零的值。如果两个字符串相等,函数就返回零。

  • 注意:由于 strcmp 并不修改它的任何一个参数,所以不存在溢出字符数组的危险。但是,和其他不受限制的字符串函数(strcpy, strcat)一样,strcmp 函数的字符串参数也必须以一个 NUL 字符结尾。如果并非如此,strcmp 就可能对参数后面的字符进行比较,这个比较结果将不会有什么意义。

  • 举例:

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

int main()
{
    char *s1 = "hello";
    char *s2 = "hello, world";
    int ans = strcmp(s1, s2);
    if (ans > 0) {
        printf("s1 > s2\n");
    } else if (ans < 0) {
        printf("s1 < s2\n");
    } else {
        printf("s1 = s2\n");
    }
    return 0;
}

运行结果: 7.png

  1. strncmp
  • 原型:int strncmp(char const *s1, char const *s2, size_t len);

  • 功能:和 strcmp 一样,也用于比较两个字符串,但它最多比较 len 个字节。如果两个字符串在第 len 个字符之前存在不相等的字符,这个函数就像 strcmp 一样停止比较,返回结果。如果两个字符串的前 len 个字符相等,函数就返回零。

  • 举例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LEN 5

int main()
{
    char *s1 = "hello";
    char *s2 = "hello, world";
    int ans = strncmp(s1, s2, LEN);
    if (ans > 0) {
        printf("s1 > s2\n");
    } else if (ans < 0) {
        printf("s1 < s2\n");
    } else {
        printf("s1 = s2\n");
    }
    return 0;
}

运行结果:

8.png

  1. strchr、strrchr
  • 原型:
  char *strchr(char const *str, int ch);

  char *strrchr(char const *str, int ch);
  • 功能:在一个字符串中查找一个特定字符。

  • 注意:第 2 个参数是一个整型值。但是,它包含了一个字符值。strchr 在字符串 str 中查找字符 ch 第一次出现的位置,找到后函数返回一个指向该位置的指针。如果该字符并不存在于 str 中,函数就返回一个 NULL 指针。strrchr 的功能和 strchr 基本一致,只是它所返回的是一个指向字符串中该字符最后一次出现的位置(最右边那个)。

  • 举例:

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

int main()
{
    char string[20] = "Hello there, honey.";
    char *ans;
    char *ans1;
    ans = strchr(string, 'h');
    printf("ans:|%s|\n", ans);
    ans1 = strrchr(string, 'h');
    printf("ans:|%s|\n\n", ans1);

    ans = strchr(string, 'a');
    printf("ans:|%s|\n", ans);
    ans1 = strrchr(string, 'a');
    printf("ans:|%s|\n", ans1);
    return 0;
}

运行结果:

9.png

  1. strpbrk
  • 原型:
char *strpbrk(char const *str, char const *group);
  • 功能:这个函数返回一个指向 str 中第 1 个匹配 group 中任何一个字符的字符位置。如果未找到匹配,函数返回一个 NULL 指针。

  • 举例:

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

int main()
{
    char string[20] = "Hello there, honey.";
    char *ans;
    ans = strpbrk(string, "aeiou");
    printf("ans:|%s|\n", ans);
    return 0;
}

运行结果:

10.png

  1. strstr
  • 原型:
char *strstr(char *s1, char *s2);
  • 功能:这个函数在 s1 中查找整个 s2 第 1 次出现的起始位置,并返回一个指向该位置的指针。如果 s2 并没有完整地出现在 s1 的任何地方,函数将返回一个 NULL 指针。如果第 2 个参数是一个空字符串,函数就返回 s1。

  • 举例:

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

int main()
{
    char s1[20] = "Hello there, honey.";
    char s2[] = "honey";
    char *s3 = "";
    char *ans;
    ans = strstr(s1, s2);
    printf("ans:|%s|\n", ans);

    ans = strstr(s1, s3);
    printf("ans:|%s|\n", ans);
    return 0;
}

运行代码:

11.png

标准库中并不存在 strrstr 或者 strrpbrk 函数。下面是其中的一个实现:

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

char *My_strrstr(char const *s1, char const *s2)
{
    char *last = NULL;
    char *current = NULL;

    /* 只在第2个字符串不为空时才进行查找,如果s2为空,返回NULL */
    if (*s2 != '\0') {
        /* 查找s2在s1中第1次出现的位置 */
        current = strstr(s1, s2);

        /* 我们每次找到字符串时,让指针指向它的起始位置,然后查找该字符串下一个匹配位置 */
        while (current != NULL) {
            last = current;
            current = strstr(last + 1, s2);
        }
    }
    return last;
}

char *My_strrpbrk(char const *s1, char const *group)
{
    char *last = NULL;
    char *current = NULL;

    /* 只在第2个字符串不为空时才进行查找,如果s2为空,返回NULL */
    if (*group != '\0') {
        /* 查找s2在s1中第1次出现的位置 */
        current = strpbrk(s1, group);

        /* 我们每次找到字符串时,让指针指向它的起始位置,然后查找该字符串下一个匹配位置 */
        while (current != NULL) {
            last = current;
            current = strpbrk(last + 1, group);
        }
    }
    return last;
}

int main()
{
    char s1[30] = "Hello there, honey.honey";
    char s2[] = "honey";
    char *s3 = "";
    char *ans;
    ans = strstr(s1, s2);
    printf("ans:|%s|\n", ans);

    ans = strstr(s1, s3);
    printf("ans:|%s|\n", ans);

    ans = My_strrstr(s1, s2);
    printf("ans:|%s|\n", ans);

    ans = strpbrk(s1, "e");
    printf("ans:|%s|\n", ans);

    ans = My_strrpbrk(s1, "e");
    printf("ans:|%s|\n", ans);

    return 0;
}

运行结果:

12.png

  1. strtok
  • 原型:
char *strtok(char *str, char const *sep);
  • 功能:分解字符串 str 为一组字符串,分隔符为 sep。

  • 注意:如果 strtok 函数的第 1 个参数不是 NULL,函数将找到字符串的第 1 个标记。strtok 同时将保存它在字符串中的位置。如果 strtok 函数的第 1 个参数是 NULL,函数就在同一个字符串中从这个被保存的位置开始像前面一样查找下一个标记。如果字符串内不存在更多的标记,strtok 函数就返回一个 NULL 指针。在典型情况下,在第 1 次调用 strtok 时,向它传递一个指向字符串的指针。然后,这个函数被重复调用(第 1 个参数为 NULL),直到它返回 NULL 为止。

  • 举例:

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

int main()
{
    char whitespace[] = " ";
    char *token;
    char line[] = "I love you";
    for (token = strtok(line, whitespace); token !=NULL;
         token = strtok(NULL, whitespace)) {
             printf("Next token is |%s|\n", token);
        }
    return 0;
}

运行结果: 13.png

11 __attribute__((format(printf, a, b))) 的用法

最近看代码的时候,发现在调用打印日志的函数的时候,发现有一些问题:要么是格式化参数的个数与实际的个数不一致,要么是参数类型不匹配。刚开始我是看到一处有错误,就修改一处。后面想了想,即使我这次把所有的都改对了,但也不能保证我后面或者其他人不再次出错。所以我想有一种方法能让以后的人写错了,也能发现错误。经过请教别人,知道了这个attribute format 的用法。现总结记录一下。

  • 功能:__attribute__ format 属性可以给被声明的函数加上类似 printf 或者 scanf 的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。format 属性告诉编译器,按照 printf, scanf 等标准 C 函数参数格式化规则对该函数的参数进行检查。

  • 语法格式:

format(archetype, string-index, first-to-check)

其中,"archetype"指定是哪种风格;“string-index”指定传入函数的第几个参数时格式化字符串(从 1 开始);"first-to-check"指定从函数的第几个参数开始按上述规则进行检查。 具体的使用如下所示:

__attribute__((format(printf, a, b)))

__attribute__((format(scanf, a, b)))

其中参数 a, b 的含义是: a: 第几个参数为格式化字符串(format string); b: 参数集合中的第一个,即参数"..."里的第一个参数在函数总数排在第几。

举例说明:

// test2.c
#include <stdio.h>
#include <stdarg.h>

#if 1
#define CHECK_FMT(a, b) __attribute__((format(printf, a, b)))
#else
#define CHECK_FMT(a, b)
#endif

void TRACE(const char *fmt, ...) CHECK_FMT(1, 2);

void TRACE(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    (void)printf(fmt, ap);

    va_end(ap);
}

int main() {
    TRACE("ivalue = %d\n", 6);
    TRACE("ivalue = %d, %s\n", 6); // 第24行
    TRACE("ivalue = %d\n", "test"); // 第25行
}

注意,编译时要加上 -Wall 才会有编译告警。

编译: gcc test2.c -Wall 编译结果:

test2.c: In function 'main':
test2.c:24:26: warning: format '%s' expects a matching 'char *' argument [-Wformat=]
     TRACE("ivalue = %d, %s\n", 6);
                         ~^
test2.c:25:22: warning: format '%d' expects argument of type 'int', but argument 2 has type 'char *' [-Wformat=]
     TRACE("ivalue = %d\n", "test");
                     ~^     ~~~~~~
                     %s

由上面的编译结果可知,当加上了attributeformat 之后,确实会减产出可变参数的类型或者个数是否正确。

如果不使用attribute format,则不会有告警。你可以将上面的代码改一下在本地试一下。

12 可变参数函数

参考文献:blog.csdn.net/linyt/artic…

C 语言的可变参数函数的定义:

type fun(type arg1, type arg2, ...);

其中 type 表示类型,arg1, arg2 表示参数名,符号"..."用来表示参数的个数以及相应的类型都是可变的,相当于多个参数的占位符,可为 0 个,1 个或多个参数,并且要求"..."前至少有一个参数, 并且它的后面不能再出现参数。 可变参数列表是通过宏来实现的,这些宏定义在标准库 stdarg.h 头文件中,这个头文件声明了一个类型 va_list 和三个宏——va_start、va_arg 和 va_end。

  • void va_start(va_list ap, last)

    last 为函数形参中"..."前的最后一个形参名字,宏 va_start 用于根据 last 的位置(或指针)来初始化变量 ap,以供宏 ar_arg 来依次获得可变参数的值。变量 ap 在被 va_arg 或 va_end 使用前,必须使用 va_list 初始化。

  • type va_arg(va_list ap, type)

    va_arg 宏用来获得下一个参数的值,type 为该参数的类型,它的参数 ap 必须被 va_start 初始化,通过该宏后,返回参数值并使用 ap 指向下一个参数,以供 va_arg 再次使用。如果没有下一个参数时调用 va_arg 或 arg 指定的类型不兼容时,会产生可知的错误。

  • void va_end(va_list ap)

    宏 va_end 与 va_start 必须要同一函数里面对称使用,调用 va_start(ap, last)后 ap 得到初始化,在完成参数处理后,需要调用 va_end(ap)来"释放"ap。

举例

#include <stdarg.h>
#include <stdio.h>

float average(int n_values, ...) {
    va_list var_arg;
    int count;
    float sum = 0;

    // 准备访问可变参数
    va_start(var_arg, n_values);
    printf("n_values:%d\n", n_values);

    // 添加取自可变参数列表的值
    for (count = 0; count < n_values; count += 1) {
        sum += va_arg(var_arg, int);
    }

    // 完成处理可变参数
    va_end(var_arg);
    return sum / n_values;
}

int main()
{
    int n_values = 4;
    float a = average(n_values, 3, 4, 5, 6);
    printf("%f\n", a);

    return 0;
}

13 define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)

参考文献: blog.csdn.net/zhanshen201…

/*
 * 选自 linux-2.6.7 内核源码
 * filename: linux-2.6.7/include/linux/stddef.h
 */
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

linux 中是定义了一个宏 offsetof,这个宏接受两个参数,一个 TYPE 代表结构体的类型,另一个 MEMBER 代表结构体中的成员,我们看看后面的宏替换部分发生了什么,先看 ((TYPE *)0) 这个部分,它把 0 这个数字强制转换成 TYPE * 型的指针类型,这样 ((TYPE *)0) 这个整体就相当于一个指针指向了 0 这个地址,不管 0 这个地址是否合法,是否真的有这么一个结构体对象,它都会把以 0 地址为首的一片连续内存当成一个结构体对象操作,那么再看 ((TYPE *)0)->MEMBER 这个部分,((TYPE *)0) 这个指针要取结构体对象中的 MEMBER 成员,因为这只是读内存的操作,并没有写入数据,所以虽说地址不合法,但并不会发生段错误,这样取到 MEMBER 成员后,前面的 & 符就可以对 MEMBER 成员取地址了,刚才我也说了,B - A 的差是偏移量的话,如果 A 等于 0,那么 B 本身就是偏移量,那正好对应现在的情况,((TYPE *)0) 本身就是以 0 地址为首进行操作,那么它取到的 MEMBER 成员所在的地址就是相对于结构体首地址的偏移量,然后再把这个地址强制转换成 size_t 类型,于是该成员的偏移量就得到了。有没有感觉很精妙。