C Tutorials-01-基础

基本数据类型

  • C/C++作为一种强类型语言, 一个变量被使用前必须被定义.
  • 在32位系统中基本类型的长度(字节): char(1B), short(2B), int(4B), 指针(4B), long(4B), float(4B), double(8B), long long(8B);
  • 在64位系统中基本类型的长度(字节): char(1B), short(2B), int(4B), 指针(8B) long(8B), float(4B), double(8B), long long(8B);
  • long 和 int 范围是[-2^31,2^31), 即-2147483648~2147483647.
  • 而unsigned范围是[0,2^32), 即0~4294967295. 也就是说, 常规的32位整数只能够处理40亿以下的数.
  • 相比于C++98标准, C++11整型的最大改变就是多了long long
    • long long整型有两种:long long和unsigned long long. 在C++11中, 标准要求long long整型可以在不同平台上有不同的长度, 但至少有64位. 我们在写常数字面量时, 可以使用LL后缀(或是ll)标识一个long long类型的字面量, 而ULL(或ull、Ull、uLL)表示一个unsigned long long类型的字面量. 比如:unsigned long long int ulli = -9000000000000000000ULL;
    • 对于有符号的, 下面的类型是等价的:long long、signed long long、long long int、signed long long int; 对于无符号的:unsigned long long和unsigned long long int也是等价的.
    • 同其他的整型一样, 要了解平台上long long大小的方法就是查看<climits>(或<limits.h>中的宏). 与long long整型相关的一共有3个:LLONG_MIN、LLONG_MAX和ULLONG_MIN, 它们分别代表了平台上最小的long long值、最大的long long值, 以及最大的unsigned long long值.
#include <climits>

int main() {
    long long ll_min = LLONG_MIN;
    long long ll_max = LLONG_MAX;
    unsigned long long ull_max = ULLONG_MAX;

    // 编译选项:g++ -std=c++11 2-2-1.cpp
    // 在代码清单中, 将以上3个宏打印了出来, 对于printf函数来说,
    // 输出有符号的long long类型变量可以用符号%lld,
    // 无符号的unsigned long long则可以采用%llu.

    printf("min of long long: %lld\n", ll_min); // min of long long: -9223372036854775808
    printf("max of long long: %lld\n", ll_max); // max of long long: 9223372036854775807
    printf("max of unsigned long long: %llu\n", ull_max);   // max of unsigned long long: 18446744073709551615
}

@ref: 结构体对齐, 位域, 柔性数组 - DOS5GW的专栏 - CSDN博客

大小端存储

大端/小端存储(big endian/little endian):

MSB=高权位,LSB=低权位,比如自然数字0x1A39,1A是MSB,39是LSB,判断大小端存储,可根据数据在内存中存储的地址是以MSB/LSB为地址,

  • 大端: LSB在高地址,MSB在低地址;
  • 小端: MSB在高地址,LSB在低地址;

比如一个int,其LSB作为此数据的首地址(内存中的低地址),则为小端存储;

比如书写顺序0x1122,11是高字节MSB,22是低字节LSB.
如果用大端存储:高地址22,低地址11;
如果用小端存储:高地址11,低地址22;

@ref: 大端(Big Endian)与小端(Little Endian)详解 - DOS5GW的专栏 - CSDN博客

运算符

  • 位与&, 位或|, 异或^, 取反~, 位左移<< , 位右移>>
  • sizeof是C语言的一种单目操作符, 如C语言的其他操作符++、–等. 它并不是函数. sizeof操作符以字节形式给出了其操作数的存储大小. 操作数可以是一个表达式或括在括号内的类型名. 操作数的存储大小由操作数的类型决定.  
    • 当操作数具有数组类型时, 其结果是数组的总字节数
    • 联合类型操作数的sizeof是其最大字节成员的字节数
    • sizeof的优先级为2级, 比乘除等3级运算符优先级高

编程语言中, 取余和取模的区别到底是什么? - 知乎
当除数和被除数不同符号时: 取余向0方向舍弃小数位, 取模向负无穷方向舍弃小数位, 比如4/(-3)约等于-1.3 :
取余: 4 rem 3 = -1;
取模: 4 mod 3 = -2;

格式化输出

格式化输出printf是一个变参函数, 原型为int printf(char *format,...) ,
C语言用宏来处理这些可变参数, 根据参数入栈的特点从最靠近第一个可变参数的固定参数开始, 依次获取每个可变参数的地址. 例如printf ("Decimals: %d %ld\n", 1977, 650000L);
需要注意的是格式要跟变量的长度对应, 比如long long要使用%ll, int类型不能使用%c格式.

extern

首先,声明与定义的区别:

  • 定义:编译器会为变量或函数分配内存 //如:int a=1;
  • 声明:只是表明其存在,但没有分配内存这个过程。//如:int a; 带或带extern

extern的两个作用:

(1) extern声明 & 定义变量和函数. 例如
在aaa.cpp 定义函数: void func();
在bbb.cpp 调用该函数前需要先extern声明: extern void func();
或者在aaa.h头文件里extern声明, 所有包含此头文件的*.cpp都可以是使用func()函数.

(2) C++项目中常见的 extern "C": 这就告诉 C++编译译器,函数 foo 是个 C库的函数,那么C++编译器应该按照C编译器的编译和链接规则来进行链接,也就是说到库中找名字_foo 而不是找_foo_int_int(原因是C++支持函数的重载)

static

(1) 当我们同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具有全局可见性。如果加了 static,就会对其它源文件隐藏(其他文件无法使用)。所以static 和 extern不能同时修饰一个变量

(2) 全局变量和 static 变量存储在 静态存储区,程序启动时被初始化为0

const

(1) 修饰局部 & 全局常量:const int num=5;

(2) 常量指针:指针指向的内容是常量,两种定义皆可:const int * num;int const * num;

(3) 指针常量:指针本身是个常量,不能再指向其他的地址 int *const num;

(4) 修饰函数的形参:

  • 防止修改指针指向的内容 void FUN(char *destin, const char *source);
  • 防止修改指针指向的地址 void FUN ( int * const p1 , int * const p2);

(5) 修饰函数的返回值:

const char * FUN(void);
char *str = FUN(); // err
const char *str = FUN();

数组

什么是数组类型?

下面是C99中原话:

An array type describes a contiguously allocated nonempty set of objects with a
particular member object type, called the element type.36) Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T , the array type is sometimes called ‘‘array of T ’’. The construction of an array type from an element type is called ‘‘array type derivation’’.

很显然, 数组类型也是一种数据类型, 其本质功能和其他类型无异:定义该类型的数据所占内存空间的大小以及可以对该类型数据进行的操作(及如何操作).

数组退化

数组类型也是一种数据类型, 其本质功能和其他类型无异:定义该类型的数据所占内存空间的大小以及可以对该类型数据进行的操作(及如何操作).
数组在某些情况下, “数组类型的变量”会退化成指针类型,
这时候无法再获取数组长度, 会影响sizeof操作符的结果,

数组什么时候会”退化”

数组在除了3种情况外, 其他时候都要”退化”成指向首元素的指针. 这3中例外情况是:
比如有数组 char s[10] = "hello";

  1. sizeof(s)
  2. &s
  3. char s[10]作为左值创建”字符串”, s仍然是数组类型

静态数组索引(C99)

下面的代码向编译器保证, 你传递给f 的指针指向一个具有至少10个int 类型元素的数组的首个元素. 我猜这也是为了优化; 例如, 编译器将会假定a 非空. 编译器还会在你尝试要将一个可以被静态确定为null的指针传入或是一个数组太小的时候发出警告.

void f(int a[static 10]) {
...
}

声明一个不可修改的数组, 这和说明符int * const a.作用是一样的

void f(int a[const]) {
...
}

指针

指针的值就是一个内存地址, 或者说,指针指向的内存区的开始地址, 指针的长度为sizeof(int)=4 (32位机),

restrict关键词是一个限定词, 可以被用在指针上. 它向编译器保证, 在这个指针的生命周期内, 任何通过该指针访问的内存, 都只能被这个指针改变. 比如,

int f(const int* restrict x, int* y) {
(*y)++;
int z = *x;
(*y)--;
return z;
}

指针的类型 & 指针指向的类型:

例子,说明以下指针的类型/指向的类型:

int* ptr;  
char* ptr;
int** ptr;
int (*ptr)[3];
int* (*ptr)[4];

*号去掉, 剩下的部分即是”指针的类型”;
将”指针名字”和”指针名字左边的指针声明符*“去掉, 剩下的部分即是”指向的类型”.

指针的加减运算

加减运算包括: 自++,–, 指针±数值, 指针±指针, 这些运算时, 指针数值的变化, 指针指向的变化.

  • 自++/–: 指针±1, 指针数值±sizeof(指针类型), 指针指向下一个元素的地址;
  • 指针±N: 指针数值±sizeof(指针类型)*N, 指针移动到后面第N个元素;
  • 指针-指针: 两个指针相隔的元素个数, 同类型的指针才可以相减;
  • 指针+指针: 无意义.

阅读下面的代码, 打印结果?:

int a[5]={1,2,3,4,5};
int *ptr1=(int *)(&a+1);
int *ptr2=(int *)((int )a+1);
printf("%x,%x",ptr1[-1],*ptr2);

第二行: &a, 数组名取地址, 相当于一个”数组指针”, 该指针的类型是int (*)[5], 指向的类型是int [5], 所以这个指针+N, 指针实际移动的字节数 = N * sizeof(int [5]), ptr1[-1]会打印出: 5

数组 & 指针的不同

char s[] = "hello";
char *p = "hello";
  1. 初始化的不同
    在第一句中,以&s[0]开始的连续6个字节内存分别被赋值为: ‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘/0’
    第二句中,p被初始化为程序data段的某个地址,该地址是字符串”hello”的首地址
  2. sizeof的不同: sizeof(s)应为6, 而sizeof(p)应为一个”指针”的大小.
  3. &取地址操作符的不同:
    &s的类型为pointer to array of 6 chars.
    &p的类型为pointer to pointer to char.

结构体

在C99之前, 你只能按顺序初始化一个结构体. 在C99中你可以这样做

struct Foo {
int x;
int y;
int z;
};
Foo foo = {.z = 3, .x = 5};

这段代码首先初始化了foo.z,然后初始化了foo.x. foo.y 没有被初始化, 所以被置为0.
这一语法同样可以被用在数组中. 以下三行代码是等价的:

int a[5] = {[1] = 2, [4] = 5};
int a[] = {[1] = 2, [4] = 5};
int a[5] = {0, 2, 0, 0, 5};

Struct

结构体字节对齐

➤ 结构体的sizeof, gcc和cl编译器有所不同, 以cl为例:

  1. 结构体变量的首地址能够被其平台的对齐字节数所整除(4字节对齐 or 8字节对齐)
  2. 结构体每个成员相对结构体首地址的偏移都是该成员大小的整数倍, 例如int成员相对结构体首地址的偏移是sizeof(int), char成员相对结构体首地址的偏移是sizeof(char). 如不满足, 会在前一个成员之后增加填充字节以满足对齐. 如果结构体成员是数组TYPE arr[3], 成员arr的首地址以sizeof(TYPE)对齐而不是sizeof(TYPE[3]).
  3. sizeof(struct)等于struct内最大基本元素长度的整数倍, 如有需要编译器会在最末一个成员之后加上填充字节(trailing padding).

考虑一下, 为什么有第3条对齐准则?如果声明一个结构体struct sArr[2];, 这样可保证sArr[1]的首地址也满足第1条

@ref: 字节对齐,看这篇就懂了 - 腾讯云开发者社区-腾讯云

➤ 包含结构体成员的结构体:

  • 在寻找最宽基本类型成员时, 应当包括“子结构体”的成员;
  • “子结构体变量”的首地址能够被其最宽基本类型成员的大小所整除;

    struct S1 {
    char c;
    int i;
    }; //sizeof(S1) = 8

    struct S3 {
    char c1;
    S1 s;  //8 bytes
    char c2
    };

    S1或S3的最宽简单成员的类型都为int, 所以S3的最宽简单类型为int;
    S3::s的类型是struct S1, 其起始地址是sizeof(int)的整数倍(struct S1最宽的成员是int型);
    S3占用内存如下:
    S3:c1占1字, 填充3字, S1:c占一字, 填充3字, S1:i占4字, S3:c2占1字, 填充3字, 故sizeof(struct S3) = 16;

➤ 改变缺省的对齐条件, 即“成员相对于结构体首地址的偏移量, 是成员大小的整数倍”, 变成了“成员相对于结构体首地址的偏移量, 是对齐字节的整数倍”. VC6中使用语法如下:

#pragma pack(push) // 将当前pack设置压栈保存
#pragma pack(2) //按照2字节对齐
struct S1
{
char c;
int i;
}; // 6 bytes
struct S2{
char  c1;
struct S1 sss;
char c2
};
#pragma pack(pop)

#pragma pack(n), 如果n比结构体成员的sizeof值小, 那么该成员的偏移量应该以此值为准, 结构体成员的偏移量应该取二者的最小值.
上面对定义中最宽的int, 和#pragma pack(2)比较, 所以对齐条件是2字节;
char S1::c占1字, int S1::i宽度是4, 这里不以4而是以2对齐, 所以int S1::i的起始位置是2, sizeof(S1) == 6.
注: 没有任何成员的“空结构体”占1byte;

含位域结构体的sizeof

使用位域的主要目的是压缩存储, 其大致规则为:

  • 1) 如果相邻位域字段的类型相同, 且其位宽之和小于类型的sizeof大小, 则后面的字段将紧邻前一个字段存储, 直到不能容纳为止;
  • 2) 如果相邻位域字段的类型相同, 但其位宽之和大于类型的sizeof大小, 则后面的字段将从新的存储单元开始, 其偏移量为其类型大小的整数倍;
  • 3) 如果相邻的位域字段的类型不同, 则各编译器的具体实现有差异, VC6采取不压缩方式, Dev-C++采取压缩方式;
  • 4) 如果位域字段之间穿插着非位域字段, 则不进行压缩;
  • 5) 整个结构体的总大小为最宽基本类型成员大小的整数倍.

柔性数组(flexible array)

  • C99中, 结构中的最后一个元素允许是未知大小的数组, 这就叫做柔性数组成员, 但结构中的柔性数组成员前面必须至少一个其他成员.
  • 柔性数组成员允许结构中包含一个大小可变的数组. sizeof返回的这种结构大小不包括柔性数组的内存.
  • 包含柔性数组成员的结构用malloc函数进行内存的动态分配, 并且分配的内存应该大于结构的大小, 以适应柔性数组的预期大小.

柔性数组到底如何使用呢?看下面例子:

typedef struct st_type
{
int len;
char data[0];
}type_a;

有些编译器会报错无法编译可以改成:

typedef struct st_type
{
int len;
char data[];
}type_a;

通过如下表达式给结构体分配内存: type_a *p = (type_a*)malloc(sizeof(type_a) + 100*sizeof(char));

这样我们为结构体指针 p 分配了一块内存(该内存块大小远大于结构的大小).
但是这时候我们再用 sizeof(*p)sizeof(type_a) 测试结构体的大小, 发现仍然为 4, sizeof返回值不包括柔性数组部分.
前面说过,数组名就代表一个地址,是一个不变的地址常量。在柔性数组的结构体中,数组名仅仅是一个符号而已,只代表一个偏移量,不会占用具体的空间。

我们用 p->data[n]就能简单地访问可变长元素.

对于柔性数组的这个特点,很容易构造出变成结构体,如缓冲区等等, 其实柔性数组成员在实现跳跃表时有它特别的用法,在Redis的SDS数据结构中和跳跃表的实现上,也使用柔性数组成员。它的主要用途是为了 满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题

当然, 上面既然用 malloc函数分配了内存, 肯定就需要用 free函数来释放内存:free(p)

需要说明的是:C89不支持这种东西, C99把它作为一种特例加入了标准. 但是, C99 所支持的是 incomplete type, 而不是 zero array, 形同 int item[0];这种形式是非法的, C99支持的形式是形同 int item[];只不过有些编译器把 int item[0];作为非标准扩展来支持, 而且在C99发布之前已经有了这种非标准扩展了, C99发布之后, 有些编译器把两者合而为一了.

@ref:

typedef

➤ 用法: typedef oldType newType:

  • typedef unsigned char BYTE; // 新定义BYTE
  • typedef struct Language { ... } LANG; // 新定义LANG
  • typedef void (*pf)(int, int); // 函数指针

typedef vs #define

  • typedef仅可用于类型, define宏还可以用于数值, 例如#define 1 ONE
  • typedef由编译器进行解释, define宏是由预编译期解释的

@ref https://www.runoob.com/cprogramming/c-typedef.html

字符串

在 C语言中,字符串实际上是使用 null 字符 \0 终止的一维字符数组。

在 string.h中提供的字符串api:

  • strcpy(s1, s2);
  • strcat(s1, s2);
  • strlen(s1);
  • strcmp(s1, s2);
  • strchr(s1, ch);
  • strstr(s1, s2);

函数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

当调用函数时,有两种向函数传递参数的方式:

  • 传值调用:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
  • 引用调用:通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

默认情况下,C 使用传值调用来传递参数。这意味着函数内的代码不能改变用于调用函数的实际参数。

可变参函数

声明方式为: int func_name(int arg1, ...); 其中,省略号 … 表示可变参数列表。

@ref: https://www.runoob.com/cprogramming/c-variable-arguments.html

内存管理

常用函数:

  • void *malloc(int num):用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
    malloc的实现 => [[C-Tutorials.02.内存管理-malloc.md]]

  • void free(void *address):用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。

  • void *calloc(int num, int size):用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存(num x size 字节)的指针。

  • void *realloc(void *address, int newsize):用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。

宏(macro)

C/C++的宏定义将一个标识符定义为一个字符串, 源程序中的该标识符均以指定的字符串来代替. 宏的替换是在程序源代码被编译之前, 由预处理器(Preprocessor)对程序源代码进行的处理.

宏主要用在宏定义和条件编译.

宏定义

宏常量

#define MAX 1000: 在《Effective C++》中, 这种做法却并不提倡, 书中更加推荐以const常量来代替宏常量. 因为在进行词法分析时, 宏的引用已经被其实际内容替换, 因此宏名不会出现在符号表中. 所以一旦出错, 看到的将是一个无意义的数字, 比如上文中的1000, 而不是一个有意义的名称, 如上文中的MAX. 而const在符号表中会有自己的位置, 因此出错时可以看到更加有意义的错误提示.

宏函数

为什么使用宏函数?

  • 宏只是在预编译期做展开(简单替换宏代码),并不像真正的函数需要额外的一次函数调用,也就节省了函数用的开销(传参、寄存器… )

与inline函数的区别? @todo

#define MAX(a,b) ((a)<(b) ? (b) : (a))

为什么大量的宏定义中用到了do-while:

#define FOO(x) bar(x); baz(x)
// 如果这样使用宏:
if (condition)
FOO(1);
// 会被替换成:
if (condition)
bar(x); baz(x); // 第二句脱离了if控制

// 改进一下, 加上大括号
#define FOO(x) { bar(x); baz(x); }
// 被替换成这样:
if (condition)
{ bar(x); baz(x); }; // 多了个分号, 编译错误

所以正确的写法:

#define FOO(x) do { bar(x); baz(x); } while (0)

// 如果这样使用宏:
if (condition)
FOO(1);
// 会被替换成:
if (condition)
do { bar(x); baz(x); } while (0);

do{...} while(condition)语句最后可以有分号也可以没有, 这两种语法上都正确

宏定义中的”#”和”##”

#的功能是将其后面的宏参数进行字符串化操作(Stringfication):

#define DEBUG_RUN(func)
do { printf("entry:"#func"\n"); func(); } while(0)

#func替换后, 作为字符串拼接, 相当于printf("entry:" + funcName + "\n")

##被称为连接符(concatenator), 用来将两个Token连接为一个Token.

struct command
{
char * name;
void (*function) (void);
};

#define COMMAND(NAME) { NAME, NAME ## _command }

// 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:
struct command cmds[] = {
COMMAND(quit),
COMMAND(help),
}

可变参数的宏

@todo

预处理器 & 条件编译

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

  • #define 定义宏
  • #include 包含一个源代码文件
  • #ifdef 如果宏已经定义,则返回真
  • #ifndef 如果宏没有定义,则返回真
  • #error 当遇到标准错误时,输出错误消息
  • #pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

条件编译

#define常与#ifdef, #ifndef, defined指令配合使用, 用于条件编译.

#ifndef _HEADER_INC_
#define _HEADER_INC_
#endif

用宏控制debug日志:

#ifdef DEBUG
printf("Debug information\n");
#endif

通过DEBUG宏, 我们可以在代码调试的过程中输出辅助调试的信息. 当DEBUG宏被删除时, 这些输出的语句就不会被编译. 更重要的是, 这个宏可以通过编译参数来定义. 因此通过改变编译参数, 就可以方便的添加和取消这个宏的定义, 从而改变代码条件编译的结果.

预定义宏

#include <stdio.h>

main()
{
printf("File :%s\n", __FILE__ );
printf("Date :%s\n", __DATE__ );
printf("Time :%s\n", __TIME__ );
printf("Line :%d\n", __LINE__ );
printf("ANSI :%d\n", __STDC__ );

}

博客旧文章