C语言的学习

253 阅读21分钟

数据和C

1,位,字节和字
位(bit):最小的存储单位,可以存储0和1。
字节(byte):计算机常用的存储单位,1字节为8位
字(word):设计计算机给定的自然存储单位,个人常用的字长为32和64位,计算机字长越长,其数据转移越快,允许内存的访问越多。

image.png 2,显示十进制,八进制和十六进制
C的整数型默认是十进制,使用%d进行表示
以八进制表示使用:%o
以十六进制表示使用:%x
打印char类型:%c
sizeof打印类型为:%zd
%d整型输出,%ld长整型输出,
%p 输出变量的内存地址,
%o以八进制数形式输出整数,
%x以十六进制数形式输出整数,
%u以十进制数输出unsigned型数据(无符号数)。
%c用来输出一个字符,
%s用来输出一个字符串,
%f用来输出实数,以小数形式输出,
%e以指数形式输出实数,
%g根据大小自动选f格式或e格式,且不输出无意义的零。
%lu:long unsigned数据类型无符号长整数或无符号长浮点数
%f:double和float类型
3,c数据类型的所有关键字

image.png 现在计算机常用设置学习
long long 占用64位,long 占用32位,short占用16位,int占用16位/32位(当字长为32位的时候为16,当字长为64的时候 32),unsigned:用于非负数的整数场景。char为字符串,占用8位。_Bool为布尔类型
转义序列小计

image.png sizeof 该方法主要计算当前数据类型的字节大小(输出的单位为字节)

字符串与格式化输出

1,字符串和字符的区别:字符串为”x“,代表’x‘和空字符 代表两个字节,而字符为’x‘一个字节
2,关于strlen()函数:计算字符串的字节长度
3,常量和c的预处理:#define :既编译时替换,明示常量 4,C90新增const关键字,表示不可变的类似java的final

# include <stdio.h>
# include <string.h>
# define MAX 100 /** 预处理使用方法 **/
int main(void){
    const int MIN = 10;
    // 定义字符串数组
    char name[40];
    printf("what is your name %d",MAX);
    return 0;
}

运算符,表达式和语句

1,关于类型转换:unsigned,signed,char,short都被自动转换成int类型。 2,关于运算符的转换是以ACSII码为主的

char ch;
int i;
float f1;
f1 = i = ch = 'C';
// 由于C对应的ASCII码为67,所以i为67
printf("ch=%c,i=%df,f1=%2.2f",ch,i,f1); // ch=C,i=67f,f1=67.00
ch = ch+1;
i = f1+2*ch;
f1 = 2.0*ch+i;
// ch+1 = D 对应ASCII为68
printf("ch=%c,i=%df,f1=%2.2f",ch,i,f1); // ch=D,i=203f,f1=339.00n
ch = 1107;
// 同理数字通过ASCII转换为对应的字母
printf("now ch = %c\n",ch); // now ch = S
ch = 80.98;
printf("now ch = %c\n",ch); // now ch = P

字符的输入和输出

1,缓冲区
缓冲分为两类:完全缓冲io和行缓冲io:完全缓冲是指当缓冲区填满时才刷新缓冲区。常见的缓存区的大小为512和4096。行缓冲io是指出现换行符时刷新缓冲区,键盘输入通常是行缓冲输入。
2,关于重定向
程序可以通过两种方式进行读取文件,方法一:使用特定的函数打开文件,关闭文件,读取文件,写入文件。方法二:设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和文件输出。

函数

在定义函数时,在调用前需要再方法前面进行定义
当存在返回值的时候,这个和java不一样的点是当形参为int类型,传入double类型也是可以的,到时候会进行转义

# include <stdio.h>
# include <string.h>
# define MAX 100
int numCount(int i, int n); // 必须要定义该方法
int main(void){
// the max 3.1 and 5.2 is 5 可知返回的是5,但是我们设的类型为int
printf("the max 3.1 and 5.2 is %d",numCount(3.1,5.2));
return 0;
    }
int numCount(int i,int n){
if(i>n) return i;
else return n;
    }
}

在操作函数内部变量时,如果出现 x=y;y=x的情况,需要加个参数进行替换

x = y;
y = x; // 此时由于x已经将y的值变成x了,所以不生效
// 正确的方式
temp = x;
x = y;
y = temp;

指针学习

指针是一个值为内存地址的变量(或者数据对象)

// ptr是变量,pooh是常量
ptr = &pooh; // 把pooh的地址赋予给ptr
ptr = *pooh; // 找出pooh的值赋予ptr

关于指针的相关运算符
1,地址运算符&:是将常量的地址赋予给变量
2,地址运算符*:是将常量的值赋予给变量
关于声明指针的学习

# include <stdio.h>
# include <string.h>
# define MAX 100
int numCount(int *i, int *n);
int main(void){
   int x = 5;
   int y = 10;
    printf("this %d %d",numCount(&x,&y),x); // 此时x的值已经变成10。
}
// 该函数传递的不是函数的值,而是地址,所以需要用&的方式进行传递
int numCount(int *i,int *n){
   int temp = *i;
   *i = *n;
   *n = temp;
   return temp;
}

image.png

数组和指针

// 普通数组的定义
int arr[] = {1,2,3};
// c99新增初始化器
int arr_2[] = {1,2,[0] = 3}; //将arr[0]换成3
// {3,4,5,}
int arr_3[] = {1,2,[0] = 3,4,5} // 会从arr[0]开始替换位置,并且1,2会被代替
// 函数原型是可以省略的,下面四种方式是等价的
int sum(int* ar,int n);
int sum(int*,int);
int sum(int ar[],int n);
int sum(int [],int);
// int i[] 并不是数组本身,而是指向ar的数组首元素指针,大小为8字节
int main(void){
   int ar[3] = {1,2,3};
   numCount(ar);
}
int numCount(int i[]){
   printf("字节大小 %zd byte",sizeof i);
   return 1;
}
// 对*相关操作的理解
int * p1, * p2, * p3;
int arr_1 = {100,200};
int arr_2 = {300,400};
p1 = p2 = arr_1; // p1 = p2 = 100
p3 = arr_2; // p3 = 300
*p1++;*++p2;(*p3)++;
// 上面三个操作,其实p1和p2为200,指向下一个数组的地址,p3为301,是在当前值进行添加

关于数组和指针的指向问题,常用的三种情况 * & 和当前值

int main(void){
   // 初始化数组
   int arr[5] = {100,200,300,400,500};
   int *p1,*p2,*p3;
   p1 = arr; // 把一个地址赋给指针
   p2 = &arr[2]; //300的内存地址赋予给p2
   // 可以得知:值的变化&p1的地址是不变的
   // p1 = 0x7ffee435e5d0,*p1 = 100,&p1 = 0x7ffee435e5c8
   printf("p1 = %p,*p1 = %d,&p1 = %p\n",p1,*p1,&p1);
   // p2 = 0x7ffee435e5d8,*p2 = 300,&p2 = 0x7ffee435e5c0
   printf("p2 = %p,*p2 = %d,&p2 = %p\n",p2,*p2,&p2);
   p3 = p1 +4;
   // p1+4 = 0x7ffee435e5e0,*(p1+4) = 500
   printf("p1+4 = %p,*(p1+4) = %d\n",p1+4,*(p1+4));
   p1++;
   // p1 = 0x7ffee435e5d4,*p1 = 200,&p1 = 0x7ffee435e5c8
   printf("p1 = %p,*p1 = %d,&p1 = %p\n",p1,*p1,&p1);
   p2--;
   // p2 = 0x7ffee435e5d4,*p2 = 200,&p2 = 0x7ffee435e5c0
   printf("p2 = %p,*p2 = %d,&p2 = %p\n",p2,*p2,&p2);
   --p1;
   ++p2;
   // p1 = 0x7ffee435e5d0,p2 = 0x7ffee435e5d8
   printf("p1 = %p,p2 = %p\n",p1,p2);
   // 一个指针减去另一个指针,当前的2代表两个int,并不是2个字节
   // p1 = 0x7ffee435e5d0,p2 = 0x7ffee435e5d8,p2-p1 = 2
   printf("p1 = %p,p2 = %p,p2-p1 = %td\n",p1,p2,p2-p1);
   // 一个指针减去一个整数
   // p3 = 0x7ffee435e5e0,p3-2 = 0x7ffee435e5d8
   printf("p3 = %p,p3-2 = %p\n",p3,p3-2);
}

关于p初始化的理解
关于下面的代码是错误的,因为在初始化
p的时候,系统只分配存储指针本身的内存,并没有分配存储数据的内容,在使用指针前,必须要初始化,或者用一个现有的地址指向它,但是可以使用malloc()函数先分配内存。

int *p;
*p =5;
// 用一个现有的地址指向它
int arr[1] = {100};
p = arr;

关于const的一些用法--后续再学习深入的时候补充。

int arr[2] = {100,200};
// 此时p是不允许被修改的
const *p = arr;
*p = 1; // 错误
p[1] = 2; // 错误
// 但是可以移动地址位置
p++; // 此时让p指向arr[1]
int arr[size] = {1,2,3,4,5};
// 这种方式和下面的方式有啥区别呢?
const int *p;
int * const p1 = arr;
// 这种方式可以值的引用
*p1 = arr[3];
// 这种方式只能引用的更改,值是改不了的
p = arr;
p = &arr[3];

关于字符串的使用

1,首先字符串的输出是通过puts方法进行输出
2,定义字符串要用双引号""
3,字符串定义大小时,一定要比字符串长度+1,因为会有个空字符串。
4,在c99之后,数组大小可以是变长的
5,字符串存储在精通存储区,程序开始时,才会为该数组分配内存,此时数据拷贝到数组中,并且有两个副本(一个在静态内存,一个在数组上)
6,指针形式(* p1)在静态存储区预留了29个元素的空间,程序开始,指针变量(* p1)留出一个存储位置(默认第一位)
7,初始化数组把静态区域的字符串拷贝到数组中,初始化指针只把字符串地址拷贝给指针

char aa[] = "whate are you doing";
puts(aa); // whate are you doing
# define MSG "I am a student"
int main(void){
  char ch[] = MSG;
  char * ch_1 = MSG;
  // 可以得知MSG,指针数组 *ch_1 "I am a student" 都指向一个内存地址
  // 而数组是独立的,因为是拷贝数组里面的
  printf("MSG address %p\n",MSG); // MSG address 0x1021a4f44
  printf("ch address %p\n",ch); // ch address 0x7ffeeda5e5d9
  printf("ch_1 address %p\n",ch_1); // ch_1 address 0x1021a4f44 
  printf("I am a student address %p\n","I am a student");// I am a student address 0x1021a4f44
}

8,指针数组和字符串数组有什么区别
1,指针数组可以进行递增操作,普通数组不能
2,数组可以改变某一个值,指针不行。例如:arr[1] = 'a';
3,二维字符串数组中,每个数组的长度是一样的,而指针数组是不定长的。

image.png 4,字符串的绝大多数使用都是通过指针数组来完成的
5,关于指针数组,是通过引用的方式指向字符的地址。可以发现p1和p2是引用到msg的地址。都指向同一个。

    char *p1 = "msg";
    char *p2;
    p2 = p1;
    // p1 address 0x7ffee69795e8 p2 address 0x7ffee69795e0
    printf("p1 address %p p2 address %p\n",&p1,&p2);
    // p1 and p2 value address 0x109289f66 0x109289f66
    printf("p1 and p2 value address %p %p\n",p1,p2);

6,关于字符串获取的使用方法

// gets()函数,目前已经在c11被摒弃了,原因在于读取字符串过大时(大于定义的数组大小),取出时会导致缓冲区溢出
   char arr[2];
   puts("this is a gril");
   // this is a gril
   // warning: this program uses gets(), which is unsafe.
   gets(arr);
// gets的替代函数-fgets()和fputs();
   // bufsize:用来指示最大读入字符数
   // fgets会读取到空字符,进行存储起来,不像gets方法直接扔掉
   char *fgets(char *buf, int bufsize, FILE *stream);
   // 用法
      char buffer[11];
      fgets(buffer,11,stdin);
      printf("输出: %s\n",buffer); // 输出的大小为10个(最大)
// c11的新增函数gets_s();
    gets_s(words,STLEN);
// scanf()函数

7,常用的字符串函数:strlen(),stract(),strcmp(),strcpy(),strncpy();

// strlen():统计字符串长度
    char *p = "I am a student";
    char arr[10] = "hello";
    printf("*p:%lu\n", strlen(p)); // 14
    printf("arr:%lu\n", strlen(arr)); // 5
// stract():拼接字符串
// **dest** -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串
// **src** -- 指向要追加的字符串,该字符串不会覆盖目标字符串。
// stract方法无法效验dest的数组大小是否可以容纳src
char *strcat(char *dest, const char *src)
//demo
char input[50] = "I love programming!";
char addon[] = " Programs love me!";
strcat(input,addon);
puts(input);// I love programming! Programs love me!
// strncat()
// 将会从字符串src的开头拷贝n 个字符到dest字符串尾部,dest要有足够的空间来容纳要拷贝的字符串。如果n大于字符串src的长度,那么仅将src指向的字符串内容追加到dest的尾部。
// 他的出现是解决内存溢出的,可以解决stract中dest如果空间没有src大的问题。
char*strncat(char* dest,const char*src,size_t n)
// demo
char input[50] = "I love programming!";
    char addon[] = " Programs love me!";
    // 因为我指定10个字符串进行拼接,所以只截取10个字符串进行尾部拼接
    strncat(input,addon,10);
    puts(input); // I love programming! Programs 
// strcmp();用于字符串比较
// 相同返回0
int *strcmp*(char *str1, char *str2);
printf("%d\n",strcmp("A","A")); // 相同时返回0
printf("%d\n",strcmp("A","C")); // 不相同时返回1,如果前面的大于后面的是返回1,否则返回-1
// strncmp():strcmp比较的两个字符串直到最后一位,而strncmp可以指定比较到哪个字符串
// 比较的参数,和比较的长度
int strncmp(const char *__s1, const char *__s2, size_t __n)
    printf("%d\n",strncmp("AB","AC",1)); // 比较第一位 返回0
    printf("%d\n",strncmp("AB","AC",2)); // 比较第二位 返回1
// strcpy()和strncpy():拷贝字符串,区别也一样,strncpy可以控制copy的大小
char * strcpy (char *dest, const char *src);
char * strncpy(char *dest, const char *src, size_t n);
// sprintf():该函数声明再stdio.h中
// char *str为要写入的字符串。
// char * format为格式化字符串。
// argument, …为变量。
int sprintf(char *str, char * format [, argument, ...]);

存储类别,链接和内存管理

存储期:静态存储期,线程存储期,自动存储期,动态分配存储期。
1,如果对象具备动态存储期,程序执行期间会一直存在,“static”
2,线程存储期用于并发程序设计,以关键字"_Thread_local"声明对象
3,自动存储区:块的作用域都是具备的,当进入变量块时,为这些变量分配内存,当退出时,释放刚才为变量分配的内存。块作用域也有精通存储期,就是在局部变量中加上static
关于作用域块:C99的新特性,没有花括号的框。
1,自动变量:声明在块或者函数头中的任何变量都属于自动存储类别。
2,寄存器变量:寄存器变量存储在cpu的寄存器中,使用register声明寄存器变量

register int count = 0;

3,外部链接的静态变量:它具有文件作用域,外部链接,静态存储期,为了指明该函数使用了外部链接的变量,可以使用关键字“extern”,如果一个源文件的外部变量在另一个源文件使用,必须要使用关键字“extern”

// 文件file_one.c 
int out_size = 10;
// 文件file_two.c
extern int out_size; // 在另一个文件引用的全局变量一定要使用extern
// 文件file_two.c
extern int out_size = 20; // 这个是错误的,由于引入的是文件file_one.c的变量,是不允许改变的

分配内存:malloc()和free() malloc是用来分配内存的,free是用来释放内存的。

   char * pcl;
   pcl = (char *)malloc(strlen("Dynamic String")+1);
   free(pcl);

限定符的学习
1,c90新增的const和volatile
2,c99新增restrict:用于提高编译器优化,编程者可提示编译器:在该指针的生命周期内,其指向的对象不会被别的指针所引用
3,c11新增_Atomic

文件的输入和输出

文件是什么:是在磁盘和固态硬盘上的一段已命名的存储区,c提供两种文件模式:文件模式和二进制模式
1,文件模式:文本模式读取文件时,把文件中的\r转换成\n,在写入文件时,把文件的\n转换成\r;
2,二进制模式:可以访问文件的每个字节。
打开文件的方式fopen()

int main(void){
  int ch;
  FILE *p;
  // r为只读,正常我们可以设置rw,读写模式
  p = fopen("hello.text","r");
  // 从指定文件中获取一个字符
  ch = getc(p);
  // 当着不到字符,则显示EOF
  while(ch != EOF){
    // 输出字符
    putchar(ch);
    ch = getc(p);
  }
}

image.png fclose():关闭指定文件

// fclose()用来查看当前的file文件是否已经关闭,必要时刻要刷新缓冲区,当文件关闭时返回0,否则返回EOF

指向标准文件的指针

image.png fseek、ftell、rewind详解
fseek:根据文件指针的位置和偏移量来定位文件指针。(只用来:定位!!!)
fseek第一个参数为流,第二个参数为偏移量,第三个参数为文件指针定位的位置
SEEK_SET 以文件开头为中心
SEEK_CUR 文件指针的当前位置为中心
SEEK_END 文件结尾为中心*/

// 用法
int main(void){
  int ch;
  FILE *p;
  char val = 0;
  p = fopen("hello.text","r");
  // 从文件头开始,读取两个字符 0代表从头开始
  /**
   * SEEK_SET:从头开始算起,第几个的位置
   * SEEK_CUR :从当前位置算起,第几个位置
   * SEEK_END:从结尾开始,可以写负数
  */
  ch = fseek(p,3,SEEK_SET);
  ch = fseek(p,3,SEEK_CUR);
  ch = fseek(p,-1,SEEK_END);
  val = fgetc(p);
  printf("val %c\n",val);
}

ftell:返回文件指针相对于起始位置的偏移量。
long int ftell ( FILE * stream );

  ch = fseek(p,3,SEEK_SET);
  val = fgetc(p);
  long v2 = ftell(p);
  // 由于当前的fseek是偏移了3,那指针的位置就是4
  printf("val %c ftell position %lu\n",val,v2);

rewind:让文件指针的位置回到文件的起始位置。
void rewind(FILE *);

  ch = fseek(p,-1,SEEK_END);
  val = fgetc(p);
  long v2 = ftell(p);
  printf("val %c ftell position %lu\n",val,v2);
  rewind(p);
  // 这个值为0
  printf("ftell position again%lu",ftell(p));

优化于fseek、ftell两种处理大文件的方式:fgetpos和fsetpos
关于标准io的工作原理 -- 还需要深刻理解
1,通过fopen调用打开文件,会创建缓冲区(如果是读写模式会创建两个缓存区),和一个包含文件和缓冲区的数据结构,以及返回一个指向该结构的指针:假设为fp。
2,通过输入函数(fgets,gets)将文件设备拷贝到缓冲区中,缓冲区的大小一般为512或者512的倍数
3,除了填充缓存区外,还要设置fp指向的结构的值,主要设置当前的位置和拷贝进缓冲区的字节数
4,当读取到最后一个字符时,结尾指示器设置为真,函数返回为EOF。

结构和其他数据形式

struct用来声明结构,可以是数组和指针。

struct color {
    char blue[10];
};
struct book {
    // 使用char的数组,必须要给对应的数组大小
    char a[10];
    int b;
    float c;
    // 嵌套声明
    struct color col;
};
int main(void){
    // 初始化
    struct book bk = {
        "语文",
        10,
        20.00,
        {"blue"}
    };
    printf("嵌套查询blue: %s",bk.col.blue);    // 打印blue
    // 其余的打印是一样的。会打印上面初始化的内容
}

struct关于指针的使用

struct color {
    char blue[10];
};
struct book {
    char a[10];
    int b;
    float c;
    struct color col;
};

int main(void){
    // 初始化
    struct book bk[2] = {
        {
        "语文",
        10,
        20.00,
        {"blue"}              
        },{
        "数学",
        10,
        20.00,
        {"read"}              
        }
    };
    struct book *p;
    // book p指向bk[0]的地址
    p = &bk[0];
    // bk[0]的地址为:0x7ffee32fa5a0,bk[1]的地址为:0x7ffee32fa5c0
    printf("bk[0]的地址为:%p,bk[1]的地址为:%p\n",&bk[0],&bk[1]);
    // p的地址为:0x7ffee32fa5a0,p+1的地址为:0x7ffee32fa5c0
    printf("p的地址为:%p,p+1的地址为:%p\n",p,p+1);
    // p的col内容 blue
    printf("p的col内容 %s\n",p ->col.blue);
    p++;
    // p++的col内容 read
    printf("p++的col内容 %s\n",p ->col.blue);
    return 0;
}

关于传递结构指针还是传递结构的选择?
1,结构的优点:使用原数据的副本,保护元数据,缺点:传递结构浪费时间和存储空间,尤其是大型结构传递
2,结构指针的优点:执行快,只传递一个引用,缺点:无法保护数据(如果使用const时可以进行数据保护的)
3,选择方向:处理小型结构的用结构方式,处理大型数据用数据指针较好。
关于伸缩型数组成员(C99)
大体用法:伸缩型数组类似普通数组,只是没有给初始大小,它的意图是想声明一个指针,指向这个伸缩型数组,然后给定初始大小

struct persion{
    int count;
    double scores[]; // 没有给特定的大小
}
int main(void){
    struct persion *p;
    // 请求一个结构和一个数组5个double大小的空间 类似 double scores[5]
    p = malloc(sizeof(struct persion) + 5 * sizeof(double)); 
}

使用伸缩型数组成员注意事项:
1,不能用结构体(含有伸缩型数组成员的结构体)进行赋值或copy;

struct var *p1; 
struct var *p2; 
*p1 = *p2; // 不要这样做!

2,不要以按值方式把这种结构体传递给其他结构体(传值调用)。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。
3,不要使用带伸缩型数组成员的结构体作为数组成员或另一个结构体的成员(结构体嵌套)。
学习和理解函数指针
函数指针:首先它是一个指针,一个指向函数的指针,在内存空间中存放的是函数的地址

int main(void){
    // 关于函数指针,函数的类型是什么,指针对应的类型必须一致,下面是void
    void (*p)(int,int) = &add;
    (*p)(3,5);
}
void add(int a,int b){
    printf("a+b=%d\n",a+b);
}

位操作

1,以二进制表示1101:12^3+12^2+02^1+12^0
2,以十进制表示12^3+12^2+02^1+12^0 = 13
3,1字节包含8位,8位中最大代表:11111111 = 128(2^7)+64(2^6)+32+16+8+4+2+1 = 255 即:最大范围255,也就是1字节可以存储0 ~ 255 一共256个值,通常:unsigned char 范围为0~255 而sign char 范围为-128 ~ +127
4,有符号整数(带有正负号的):10000001 表示-1,00000001表示1 取值范围为 -127 ~ +127(原码、反码、补码的产生过程就是为了解决计算机做减法和引入符号位的问题)

  • 4-1:原码(原码表示不便于实现加减运算):用最高位表示符号位(1:负数,0:代表正数),其他位数放二进制的绝对值
    例如:四位 1010的原码:02^2+12^1+0*2^0=2,由于首尾为1代表负数,所以为-2 原码的加减法也很简单: 0001+0010=0011,1+2=3;
  • 4-2:反码(反码表示在计算机中往往作为数码变换的中间环节):正数的反码还是等于原码;负数的反码就是它的原码除符号位外,按位取反
    例如:3的反码,由于是正数所以和原码保持一致:0011,-3的反码由于是负数,所以除了符号位其余取反,既:1100
  • 4-3:补码:正数的补码等于它的原码;负数的补码等于反码+1
    关于补码的形成:我们关注一个时钟,当前时间为10点,想要到达8点会如何操作,正常理解在转个10小时就会到8点了,因为时钟是12小时的,12小时后又是10点,所以12-(10-8)=10小时
    另一种思维:就是因为我们自认为时钟是顺时针转动的,如果逆时针呢?那也就相当于-2个小时也会到达8点,这两种思维就是补码的思维
    例如:1100的补码为:反码:1011 +1 = 1100 如果首位为0的话,那补码是不变的:0100的补码为0100
    5,关于位运算
  • 5-1 :左移:二进制数相左移动位数,相当于2 例如:10 << 1 = 20(移动1位相当于2)
  • 5-2:右移:二进制向右移动一位,相当于除以2(负数除外)