本文整理C语言基础知识,用于开发中日常查阅。
类型 | 描述 |
---|---|
基本类型 | 它们是算术类型,包括两种类型:整数类型和浮点类型 |
枚举类型 | 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量 |
派生类型 | 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。 |
void类型 | 类型说明符 void 表明没有可用的值。 |
Code格式 | 描述 |
---|---|
%c | 字符 |
%d | 带符号整数 |
%i | 带符号整数 |
%e | 科学计数法,使用小写e |
%E | 科学计数法,使用大写E |
%f | 浮点数 |
%g | 使用%e或%f中较短的一个 |
%G | 使用%E或%f中较短的一个 |
%o | 八进制 |
%s | 一串字符 |
%u | 无符号整数 |
%x | 无符号十六进制数,用小写字母 |
%p | 一个指针 |
%n | 参数应该是一个指向一个整数的指针指向的是字符数放置的位置 |
%% | 一个%符号 |
char *p = "hello"; // 字符串常量池地址赋值给p,即p指向了字符串常量池的hello
char *p1 = "hello";
char c[10] = "hello"; // 是把字符串常量池里的hello strcpy给了c
printf("%p, %p, %p\n", p, p1, c); // p和p1指向同一个地方
printf("%d, %d, %d, %d\n", (p == p1), (*p == *p1), (c == p), (*c == *p));
printf("%c, %c\n", *c, *p);
c[0] = 'H'; // 可以赋值,在栈空间上
// p[0] = 'H'; // 会崩溃,在常量池里,不可写
puts(c);
char *p2 = malloc(10 * sizeof(char));
strcpy(p2, "hello");
p2[0] = 'H';
puts(p2); // 可以修改,在堆上分配
printf("===");
char *p3 = malloc(10 * sizeof(char));
p3 = "hello"; // 把常量池hello地址赋给p3,造成内存泄露,刚才在堆上分配的10个空间大小没法释放
// p3[0] = 'H'; // 不能修改,因为是常量池的
puts(p3); // 可以修改,在堆上分配
p = "world";
strcpy(c, "world"); // 重新给c赋值
puts(p);
puts(c);
int (*p)[2]
: 数组指针,pz指向一个内含2个int类型值的数组// 传递数组指针,列数为2;
void printArr(int (*p)[2], int row) {
// *p指向行,*(*p)指向具体元素
for (int i = 0; i < row; ++i) {
for (int j = 0; j < sizeof(*p) / sizeof(int); ++j) {
printf("%d,", *(*(p + i) + j)); // *p指向行,*(*p)指向具体元素
printf("%d,", p[i][j]); // 同样可以这么访问
}
printf("\n");
}
}
void test() {
int arr[10] = {1, 2, 3}; // 未初始化元素为0
int len = sizeof(arr) / sizeof(int); // 40 / 4
int *p1 = arr; // 指向数组第一个元素的指针
int *p2 = &arr[0]; // 指向数组第一个元素的指针
for (int i = 0; i < len; ++i) {
printf("%d, %d;", p1[i], p2[i]);
}
int (*parr)[] = &arr; // 数组指针,[]运算符优先级高于*运算符,所以要加()
for (int i = 0; i < len; ++i) {
printf("%d, ", (*parr)[i]);
}
int zippo[4][2] = {{2, 4},
{6, 8},
{1, 3},
{5, 7}};
int (*pz)[2]; // 数组指针,pz指向一个内含2个int类型值的数组
pz = zippo; // 该二维数组首地址就是一个数组指针
printArr(zippo, 4);
printArr(pz, 4);
}
int *p[2]
:指针数组,长度为2的int指针变量数组。 char *a = "1";
char *b = "2";
char *arrA[] = {a, b}; // 字符串数组,用指针数组来表达
char *arrB[] = {"1", "2"}; // 字符串数组,用指针数组来表达
printf("%s,%s\n", arrA[0], arrA[1]);
printf("%s,%s\n", arrB[0], arrB[1]);
int a = 1, b = 2, c = 3;
int *arr[] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
printf("%d", *arr[i]);
}
// ()运算符优先级大于*运算符
int (*pfun)(int, int); // pfun是一个指向返回值为int的函数的指针,该函数有两个int形参
int *pfun(int, int); // pfun是一个返回值为整型指针的函数
int a = 1;
int b = 2;
// 常量指针,指针指向的值不可以改,但指针的指向可以改
const int *p = &a;
// *p = b; 错误,指向的值不可以改
p = &a; // 指针的指向可以改
// 指针常量,指针的指向不可以改,指针指向的值可以改,
int *const p2 = &a;
*p2 = b; // 指针指向的值可以改
// p2 = &b; 错误,指针的指向不可以改
// const修饰指针和常量,都不可以改
const int *const p3 = a;
// *p3 = b; 错误
// p3 = &b; 错误
struct student s = {1001, "HH", 'M'};
struct student *p = &s;
printf("%d, %s, %c\n", p->num, p->name, p->sex); // 指针用成员选择符->
printf("%d, %s, %c\n", s.num, s.name, s.sex); // 对象用成员操作符.调用
printf("%d, %s, %c\n", (*p).num, (*p).name, (*p).sex); // 成员操作符.优先级高于*,所以要加括号
&
取地址操作符,通过该操作符可以获取一个变量的地址值;
取值操作符为*
,也叫解引用,通过该操作符可以拿到一个地址对应位置的数据;“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。也就是说,解引用是返回内存地址中对应的对象。
指针和引用是一个概念,通常我们说某某变量的引用,即是说该变量的指针。
但是指针和指针变量是两个概念。即一个变量存放另一个变量的地址,那么该变量就叫做指针变量。
指针长度32位下是4个字节,64位下是8个字节。
指针的本质就是间接访问。
// 需要区分清楚指针++的操作
int arr[3] = {1, 5, 9};
int *p;
int j;
p = arr;
j = *p++;
// j = (*p)++;
// j = *++p;
// j = ++*p;
// j = *p++;
printf("arr[0]=%d, j=%d, *p=%d\n", arr[0], j, *p);
j = p[0]++;
printf("arr[0]=%d, j=%d, *p=%d, p[0]=%d", arr[0], j, *p, p[0]);
从变量的作用域角度来说可以分为全局变量和局部变量;从变量值存在的时间角度来说可以分为静态存储方式和动态存储方式;
静态存储方式:指在程序运行期间由系统分配固定的存储空间方式;
动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式;
有以下存储类别,根据变量的存储类别可以知道变量的作用域和生存期
auto
:不声明为static的局部变量都是动态存储方式,默认修复符为auto。在函数结束时自动释放这些存储空间。
static
:在静态存储区分配空间,在编译期间进行赋值,只初始化一次。
extern
:借用在别的文件定义的变量,表明这是一个声明不是定义,让编译器去别处查询其定义;
register
:寄存器变量,很少用;
tips:
函数默认是extern的;
static修饰函数表示在本文件内有效;
static修改变量表示是静态变量,在本文件内有效;
静态局部变量:虽然是静态的,但其它函数无法引用它,并指导程序终止才销毁;
静态局部变量
在count_calls函数第一次调用时,ctr被创建并初始化为0;
之后的每次调用都会将ctr加1并返回新值,故函数结束后ctr值为10;
size_t count_calls() {
static size_t ctr = 0; // 调用结束后,这个值依然有效
return ++ctr;
}
int main() {
for (int i = 0; i < 10; ++i) {
count_calls();
}
return 0;
}
typedef
允许程序员为现有类型创建别名,可以做到代码即注释;
通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写。
用typedef来命名一个结构类型时,可以省略该结构的标签:
使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签。
#define
又称宏定义,标识符为所定义的宏名,简称宏。
其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。预编译又叫预处理。预编译不是编译,而是编译前的处理。这个操作是在正式编译之前由系统自动完成的。
typedef
和#define
的区别
与#define不同,typedef创建的符号名只受限于类型,不能用于值。
typedef由编译器解释,不是预处理器。
在其受限范围内,typedef比#define更灵活。
#define PI 3.1415926 // 可以用于值
typedef int SOCKET; // 给int命个别名SOCKET,之后代码里可以使用SOCKET来表示socket的fd,做到了代码即注释,方便阅读
#define SOCKET int //等同于上句
typedef char* STRING;
STRING a,b; // 等同于 char *a, char *b
#define STRING char*
STRING a,b // 等同于char *a, b; 只有a是指针,文本替换而已;
#
表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。
头文件是一个文件,通常是源代码的形式,由编译器在处理另一个源文件的时候自动包含进来。
头文件中一般放的是同名.c文件中定义的变量、宏、系统全局变量、函数原型的声明,需要让.c外部使用的声明。
#include
这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。
#include "file"
这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。
头文件中添加 #ifndef/#define/#endif 是为了防止该头文件被重复引用。
被重复引用
是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。
比如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复引用。
头文件被重复引用引起的后果:
#ifndef HEADER_FILE
意思是"if not define HEADER_FILE" 如果不存在HEADER_FILE
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
这种结构就是通常所说的包装器 #ifndef
。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
#define 定义一个预处理宏
#undef 取消宏的定义
#if 编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef 判断某个宏是否被定义(#define过), 若已定义, 执行随后的语句
#ifndef 与#ifdef相反, 判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用, 判断某个宏是否被定义
ifdef的使用和#if defined()的用法是一样的。
ifndef又和#if !defined()的用法一样。
#pragma 说明编译器信息
#warning 显示编译警告信息
#error 显示编译错误信息
结构体对齐;
结构体在函数中传递是值传递,直接拷贝所有内容到栈空间上,所以不会改变调用方的值;需要使用指针传递引用,引用传递就可以避免这种问题。
typedef struct student {
int num;
struct student *pNext; // 此时不能省略student结构体标签
} stu;
/*
* 符号常量,简单替换,使用时要注意。
*/
#define PI 3 + 2
// #define PI (3 + 2),这样就对了
void test(void) {
int i = PI;
int j = PI;
printf("i = %d, j = %d\n", i, j);
int k = PI * PI; // 3 + 2 * 3 + 2 = 11
printf("k = %d", k);
}
补码就是为了表示负数
补码 = 原码取反 + 1
0000 0000 0000 0000 0000 0000 0000 0101 5
1111 1111 1111 1111 1111 1111 1111 1011 -5 // 取反+1
void test() {
int i = 0x80fb0000; // 首位为符号位
unsigned int ui = 0x80fb0000;
short j = 32767; // 0111 1111 1111 1111
long k = 0;
printf("%d, %d, %d\n", sizeof(j), sizeof(i), sizeof(k));
printf("%d, %u\n", i, ui); // 使用%u输出unsiged
printf("%d\n", j);
++j;
printf("%d", j); // 溢出,使用时防止溢出
}
char c = 'c'; // 1字节
printf("%c\n", c);
printf("abc\rd\n"); // \r回到行首,只输出d
printf("abc\b\b"); // 退格
char a = '\0'; // 空字符,用于标示字符串结尾,不是空格,打印不出
printf("%c", a);