【C1】基本数据类型、流程控制

2021年11月20日    Author:Guofei

文章归类: 语言    文章编号: 10001


版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2021/11/20/c1.html

Edit

编译

gcc

gcc -o main main.c
# 然后执行 main 文件
./main

# (或者)编译+链接
gcc -o main.o main.c -c
gcc -o main main.o

include

#include <stdio.h> // 表示文件在系统目录下
#include "a.h" // 表示文件在当前目录下

知识

  • main函数是执行的起点,有且只能有1个
  • printf 是向标准设备输出,未必是向屏幕输出,例如 ./main > save.txt
  • 建议用 int main,而不是 void main,后者不支持c++
  • 约定 return 0; 代表执行成功,return -1; 代表执行失败
  • 预编译:把 include 替换进来,删除注释,等操作。gcc -o main1.c main.c -E
  • 汇编 gcc -o main.s main.c -S

基本数据结构

一个二进制位是一个bit,8 bit=1 Byte

c 本身不规定变量类型的范围,跟系统有关,我的 MacBook 如下:

修饰符 数据类型 占用(字节) 取值范围
signed short int 2 -32768 到 32767 (-2^15 ~ 2^15-1)
int 4 -2147483648 到 2147483647 (-2^31 ~ 2^31-1)
long int
long long int
8
unsigned short int 2 0 到 65535 (0 ~ 2^16-1)
int 4 0 到 4294967295 (0 ~ 2^32-1)
long int 8
float 4 1.4E-45 ~ 3.4E+38,-1.4E-45 ~ -3.4E+38
duble 8 4.9E-324 ~ 1.7E+308, -4.9E-324 ~ -1.7E+308
char 1 -128~127
unsigned char 1 0~255
#include <stdio.h>
#include <float.h>

int main() {
  printf("int 存储大小 : %lu \n", sizeof(int));
  printf("a 存储大小 : %lu \n", sizeof(a));
  printf("float 存储最节数 : %lu \n", sizeof(float));
  printf("float 最小值: %E\n", FLT_MIN);
  printf("float 最大值: %E\n", FLT_MAX);
  printf("精度值: %d\n", FLT_DIG);

    return 0;
}

编码

  • 原码:最高位0代表正,1代表负
  • 反码:如果是正数,反码和原码相同。如果是负数,符号位为1,其它各位与原码相反
  • 补码:正数与原码相同。负数:反码加一
    • 好处:相加不用再考虑正负
    • 负数补码转原码:符号位不动,其它求反,得到的数加一
    • 内存中以补码的形式存储

进制

int a;
// 以 n 进制赋值
a = 10; // 默认十进制
a = 010; //0开头为8进制
a = 0b101; // 0b 开头为 2 进制
a = 0x2f;//    0x开头为16进制

// 以 n 进制显示
printf("十进制 = %d,八进制 = %o, "
       "十六进制小写 = %x , 16进制大写 = %X, "
       "十进制无符号 %u", a, a, a, a, a);


char c = 0x41;
printf("size = %lu, value= %c", sizeof(c), c);




小数

float f = 123.4f;
double d1 = 100.1; // C语言默认都是 double


char ch = 'A';
printf("字符:%c,对应的ascii:%d\n", ch, ch);

类型转换

int x = 1;
double y;
y = (double) x;

字符串

// char 的 \b 是退格符号,例如:
printf("hello word");
char b='\b';
printf("%c%c",b,b);
// 输出 hello wo


// 字符串 s/S char/wchar
printf("%s\n", "hello");


// p 16进制指针
int a = 1;
printf("%p\n", &a);

// 输出一个百分号
printf("%%", );

// printf 还有对其之类的操作,就不多写了

常量:不可被修改

// 宏常量,规范:用大写
#define MAX 100;

// const 常量
const int a = 0;
// c用宏常量多,C++用const多

"字符串常量"
20 // 整数常量

volatile,不让编译器自动做优化,例如:

int a =1;
a = a+1;
a = a+2;
// 以上代码会被编译器自动优化为: a = a+3

volatile int a = 1
// 编译器不会再自动做优化,

register:提升效率

register int a = 0; // 这个变量直接放在寄存器中,而不是内存中
a = a + 1;
// 对应汇编
move eax, 0
add eax, 1

// 普通的:
int a = 0;
a = a + 1;
// 对应汇编
move b, 0
move eax, b
add eax, 1
move b, eax

知识:

  • register 是建议型指令,而不是命令型的。如果寄存器不够用,register 可能不生效

复杂类型

struct // 结构体
union // 共用体
enum // 枚举

typedef // 声明类型别名
sizeof // 得到类型大小,32位系统返回 unsigned int,64位系统返回 unsigned long
#include <stdio.h>

enum month {
    JAN = 1, FEB = 2, MAR = 3, APR = 4, MAY = 5, JUN = 6,
    JUL = 7, AUG = 8, SEP = 9, OCT = 10, NOV = 11, DEC = 12
};

int main() {
    enum month lastmonth, thismonth, nextmonth;
    lastmonth = APR;
    thismonth = MAY;
    nextmonth = JUN;
    printf("%d %d %d \n", lastmonth, thismonth, nextmonth);
    return 0;
}

小知识:

  • 如果不定义值,第0个默认为0,后面的是前面的加1
    enum month {
      JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    

运算符

+-*/ 加减乘除
% 取模
i++ , i--  自加/自减

=,+=,-=,*=,/=,%= 赋值

int x, y, z;
x = y = z = 5; //为三个变量同时赋值
// 原理是 (z = 5) 除了赋值外还会返回5
// 所以 if(z=5) 不会报错,如果本意是 if(z == 5),会有问题

逻辑运算符

C语言没有bool类型,使用 0 代表假,非 0 代表真

// 比较运算符,返回0或1
==!=<><=>=

// 与或非
&&||!

// 三目运算符
max = x > y ? x : y;

小知识:

  • 逻辑运算符不会做额外运算,例如:3>1||x/0 不会报错,因为第二部分就不会运算

位运算符

& 按位与
| 按位或
~ 按位反
^ 按异或

<< 移位
>> 移位

存储相关

auto
static
register
extern
const
volatile

流程相关

continue
break
goto

选择语句

if语句

// 第一种
if (/* condition */) {
  /* code */
} else if (/* condition */) {
  /* code */
} else {
  /* code */
}
// 1. else if 和 else 都可以省略
// 2. if(z=5) 不会报错,如果本意是 if(z == 5),会有问题
// 3. if(a==b); 多一个分号也不会报错,只不过会直接结束if,后面的代码不在 if 范围中了。

// 第二种

if (/* condition */) {
  /* code */
}



// 第三种

if (/* condition */) {
  /* code */
} else {
  /* code */
}

// 第四种

if (/* condition */) {
  /* code */
} else if (/* condition */) {
  /* code */
} else {
  /* code */
}

switch

int a;
printf("input a=");
scanf("%d", &a);

switch (a) {
    case 0:
        printf("input 0");
        break;
    case 1:
        printf(" and 1");
        break;
    case 2:
        printf("other");
        break;
    default:
        printf("not known!");
}
// 1. break 必须带,不然的话,匹配成功一个,下面的每个 case 都会执行(不知道为啥要这样设计)

循环结构

for (size_t i = 0; i < count; i++) {
  /* code */
}


do {
  /* code */
} while(/* condition */);


while (/* condition */) {
  /* code */
}

goto:不要用

goto label1;
printf("hello");
label1:
printf("world");

关键词:

continue; // 直接执行下一个循环
break; // 跳出循环

数组

int a[10] // 定义了一个数组,其长度为10
// 1. 数组的元素类型必须都一样
// 2. 数组名本身是数组第一个元素的地址对应的常量
printf("%p == %p", a, &a[0]); // 两个都是在内存中的开头位置


// 初始化
int a[3] = {1, 2, 3};
int a[3] = {1, 2}; // 没定义的,默认为0
int a[] = {1, 2, 3}; // 如果不指定长度,自动计算和指定长度

// 3. 语法上,index 可以溢出,例如
a[100] = 999;
a[101];

// 4. 如何得到数组的长度? sizeof(a)/sizeof(a[0])
// 所以这样遍历数组
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
    printf("%d\n", a[i]);
}

二维数组

int a[2][5]; // 定义了一个二维数组,a[0],a[1] 是两个1维数组名
printf("%p = %p = %p", a, a[0], &a[0][0]); // 同一维数组,存放的也是内存地址

// 初始化
int a[2][5] = { {1, 2, 3, 4, 5},
               {3, 4, 5, 6, 7} };

// 省略的自动补0
int a[2][5] = { {1, 2, 3, 4, 5},
              {3} };

// 可以省略第一个大小
int a[][5] = { {1, 2, 3, 4, 5},
               {3} };

多维数组

int a[2][3][5]; // 定义了一个三维数组,其 a[0], a[1] 分别代表2个二维数组

字符相关

字符数组

char a[2];
a[0] = 'a';
a[1] = '\0'; // 它的 ascii 码就是 0,所以也可以 a[1] = 0;

char a1[10] = "abc"; // 这其实是初始化一个字符数组。不能用赋值 a="abc" 是错的
char a2[] = "abc"; // 会多一位 \0,这个例子实际上长度为 4

字符数组和字符串的区别:

  • 字符串是一种连特殊的 char [],它必须以0结尾
  • char[] 如果中间某个值为0,字符串就到此截断,但整体仍然是个数组
    char a[] = "hello, world";
    a[3] = 0;
    printf("%s", a); // 打印 hel
    
  • 如果一个 char[] 没有以0结尾,它就不是个字符串,printf会出乱码

字符串数组和指针

char a[100] = "hello world";
char *s = "hello world"; // 这个是先定义一个 字符串常量,然后用指针指向它的首地址。所以是只读。

例题:如何合并两个字符串?

char a[100] = {0};
char b[100] = {0};
char c[200] = {0};

scanf("%s", a); // 这里不是 &a
scanf("%s", b); // 如果用户的输出超过100个,会有溢出

int idx1, idx2;
idx1 = idx2 = 0;
while (a[idx1]) {
    c[idx1] = a[idx1];
    idx1++;
}
while (b[idx2]) {
    c[idx1] = b[idx2];
    idx1++;
    idx2++;
}

printf("%s", c);

字符串scan和print

printf

  • %s 输出一个字符串
  • %c 输出一个字符
  • %d 以十进制输出一个有符号整型
  • %u 以十进制输出一个无符号整型
  • %o 以八进制输出一个整数
  • %x 以十六进制输出一个小写整数
  • %X 以十六进制输出一个大写整数
  • %f 以十进制输出一个浮点数
  • %e 以科学计数法输出一个小写浮点数
  • %E 以科学计数法输出一个大写浮点数

scanf 有一些问题:

  1. 认为空格和回车,以及其它终止符都是结束,终止符号:
    • 0x20 空格
    • \t 水平制表符(tab键)
    • \n 换行
    • \v 垂直制表符
    • \f 换页
    • \r 回车
  2. 如果 char [10] a,然后 scanf 输入长度为3的字符串,会添加0之后赋值给前几个元素,后面的元素保持不变。
  3. 如果用户输入超过 a 的长度,会溢出
putchar('a') \\ 一次输出一个字符
printf("hello") \\ 不多说

getchar() \\ 返回 int,是你输入的字符对应的 ascii 

// scanf:
int a;
scanf("%d", &a); // 第二个变量是变量的地址
printf("%d\n",a);
  • gets 和 puts 底层是由 putchar/getchar 实现的

char a[10];
gets(a);
// 1. 用户输出超长后会报错
// 2. 空格不代表结束,回车代表结束
// 3. 仍然有溢出危险

fgets(a, sizeof(a), stdin);
// 1. 用户超长后不报错,而是截断
// 2. 回车和空格都会放进去
// 3. 没有溢出危险



puts(a); // 跟 printf 功能差不多,自动加一个 \n,所以和 gets 连用会出现两次 \n
fputs(a, stdout);
// 1. 不会自作主张加 \n

string.h

  • 还有一类print/scan
// 字符串格式化:sprintf
char a[100];
sprintf(a, "%d: Hello, %s, welcome.", 1, "Tom");
// 把格式化后的字符串,放入a
// 可以这么理解:printf 是输出到标准输出设备,sprintf 是输出到字符数组
// 额外一个小应用:int 转 char[]

// 从指定格式中的字符中提取:sscanf
char a[] = "12+21";
int i, j;
sscanf(a, "%d+%d", &i, &j);
// 额外一个小应用:char[] 转 int(其实也能转 double)
// 也可以 char c;sscanf(a, "%d%c%d", &i, &c, &j);

// 字符串中提取数字
char a[100] = "105hellohello";
char *stopstring;
long int i = strtol(a, &stopstring, 6);
// 1. 以 6 进制提取
// 2. stopstring 是提取后的剩余部分
// 3. 类似的有这些:strtof, strtol, strtold, strtold, strtoll 等
// 4. 类似的还有 atoi, atof 等
// 5. 没有反过来转化的内置函数,可以用 sprintf 实现
场景输出输入备注
字符,std_ioputchar('a')int a = getchar();
字符,文件char c=getc(p);putc(text[i], p);c==EOF为终止条件
同上fgetc(p)fputc(p)
字符串,std_iochar a[10];
     gets(a);
puts(a)有溢出风险,回车也有问题,不要用
字符串,std_iochar a[10];
fgets(a, sizeof(a), stdin);
fputs(a, stdout);建议使用
fgets会提取末尾的换行符,fputs不会自动添加换行符
字符串,文件char buf[1024] = {};
     fgets(buf, sizeof(buf), p); // 这里一次读取一行
fputs(a, p);feof(p)=1 判断到了结尾
字符串格式化,std_ioscanf("%d", a);
不会把换行符号加入到a中
   
   printf("%d\n",a);
不要提取%s,会有问题
字符串格式化,char arraychar a[] =   "12+21";
     int i, j;
     sscanf(a, "%d+%d", &i, &j);
sprintf(a, "%d: Hello, %s, welcome.", 1, "Tom");同上的问题
字符串格式化,文件fscanf(p1, "%d%c%d=\n", &a, &b, &c);fprintf(p2, "%d%c%d=%d\n", a, b, c, a + c);同上的问题
feof(p)=1 判断到了结尾

常用字符串方法

#include<string.h>

字符串长度:strlen

unsigned long len = strlen(a);
// 1. 这个长度不包括末尾的 0
// 2. 1个汉字算3个
// 3. 到第一个0为止,既是数组后面还有内容

字符串合并:strcat,strncat

strcat(a, b); // 把 a 和 b 合并,并且放到 a
// a 必须有足够的空间,否则报错
strncat(a, b, 3); // 最多追加 b 的前 3 个字符

字符串拷贝:strcpy,strncpy

strcpy(a, b); // 把 b copy 到 a 中
// 如果超过 a 的大小,不会报错,而是会继续写入后续的内存(或许会导致错误)
strncpy(a, b, sizeof(a) - 1);
// 最多只复制 sizeof(a) - 1 个,可以防止溢出

字符串比较:strcmp,strncmp

int is_unequal = strcmp(a, b);
// 如果不同,返回非0。如果相同,返回0
int is_unequal = strncmp(a, b, 2);
// 比价前2个字符串
// a == b 是不对的,因为这个比较的是内存

字符串查找:strchr,strstr

char a[] = "hello world!";
char *s; //定一个一个 char类型指针变量
s = strchr(a, 'w');
// 1. s 也是一个 char[]
// 2. printf("%s", s); 输出 world!,也就是遇到 \0 才结束
// 3. 如果找不到,返回 null,用 s==NULL 做判断

s = strstr(a, "wor"); // 功能相似,入参可以是字符串

字符串分割:strtok

char a[100] = "abc_efg_123_666666";
char *s;

s = strtok(a, "_"); // 返回 abc
printf("%s\n", s);

s = strtok(NULL, "_");// 继续查找,返回 efg。如果查完了,返回null
printf("%s\n", s);

// 使用示例:
char a[100] = "abc_efg_123_666666";
char *s;

s = strtok(a, "_");
while (s) {
    printf("%s\n", s);
    s = strtok(NULL, "_");
}

char的一些有关技巧

// 小写转大写的技巧
char a = 'a';
if (a >= 'a' && a <= 'z') { // 1. char 可以当成 int 用
    a -= ' '; // 2. 减空格就代表着转大写(ascii设计不错)
}

// 同样原理,可以字符转数字
int a = '1';
if (a >= '0' && a <= '9') {
    a -= '0';
}

函数

  1. 使用前必须先定义或声明。可以先声明,然后在底下定义。
  2. 任何位置执行 exit(0) 会让整个程序中止
  3. 字符数组作入参是这样的 int my_func(char *a)

多文件编译

// my_utils.c:
int my_add(int a, int b) {
    return a + b;
}

// main.c:
#include<stdio.h>

int my_add(int a, int b);

int main() {
    int a = 1;
    int b = 2;
    int c = my_add(a, b);
    printf("%d\n", c);
    return 0;
}

// 编译:
// gcc -o my_package main.c my_utils.c
// 执行:
// ./my_package

往往这么做

// my_utils.h
int my_add(int a, int b) {
    return a + b;
}

// main.c
#include<stdio.h>
#include "my_utils.h"

int main() {
    int a = 1;
    int b = 2;
    int c = my_add(a, b);
    printf("%d\n", c);
    return 0;
}

// gcc -o my_package main.c
// ./my_package

另外,好像还有个规范:.h 文件只声明函数,函数定义放到 my_utils.c

#include<stdio.h>

#define TEST// 定义了一个宏

int main() {
#ifdef TEST // 如果定义了 TEST,就编译这里面的,否则不编译
    printf("dev");
#endif

#ifndef TEST
    printf("prod") // 如果没定义就不编译,定义了就编译
#endif

    return 0;
}

宏的应用,在一个头文件中,一般这么做,以防重复 include 时,被重复声明(类似单例模式)

#ifndef _MY_UTILS
#define _MY_UTILS

int my_add(int a,int b){
return a + b;
}

#endif

还有一个规范:.h 文件一般只放函数的声明,.c 文件放函数的定义

一个错误的使用:

#include <stdio.h>

int func1();

int main() {
    func1(); // 这里编译器不会提示错误,但会产生一个随机的值
    return 0;
}

int func1(int a){
    printf("%d",a);
    return 0;
}

对于真的无入参函数,规范:使用 int func1(void);,C++ 没有这个问题。

标准库-内存操作

string.h

memset:内存置空

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
memset(a, 0, sizeof(a));
// 入参:置空的区域的首地址,0,这块内存大小(单位 Byte)

memcopy:内存拷贝。要确保没有内存重叠区域

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int b[10] = {0};
memcpy(b, a, sizeof(a));
// 入参:目标地址,源地址,拷贝的大小(单位 Byte)

标准库

随机数

#include<time.h>
#include<stdlib.h>

srand(100); // 指定种子
int a = rand(); // 如果不定义种子,不会随机定种子

// 如何得到真随机:
int t=(int) time(NULL);
srand(t);
int a = rand();

// 如何得到0-100的随机数?
rand() % 101;

system

#include <stdlib.h>

int main()
{
    system("ls -l"); // 运行成功返回0
    return 0;
}

system 返回什么?

  • 被调用者的返回
  • 如果调用 linux 命令,成功返回0
  • 如果调用其它编译好的 another.c,返回 another.c#main 函数的 return (???并不是)

您的支持将鼓励我继续创作!
WeChatQR AliPayQR qr_wechat