C语言基础补充

编译器四个步骤

  1. 预处理:头文件展开和宏定义展开条件编译

    gcc -E hello.c -o hello.i

  2. 编译:语法检查,生成汇编代码

    gcc -S hello.i -o hello.s

  3. 汇编:声明目标代码,无法执行,因为缺少链接

    gcc -c hello.s -o hello.o

  4. 链接:链接动态库,比如windows中的dll文件

    gcc hello.o -o a.out

为什么要声明变量

提前说明占用多大空间。

原码 补码 反码

原码和普通数字的区别:负数最高位是1,其余7位保持等价。(共八位)
反码和原码的区别:负数最高位不变,其余为取反。
补码和反码的区别:补码=反码+1

补码的意义:符合位和其他位可以等同运算,如果最高位产生进位,则可以舍弃

补码求原码:(下面的步骤仅对于负数,正数不变)

  1. 最高位不变,其他位取反
  2. 在1的基础上加1
    例如:
    char a = 0x81;
    printf("%d\n", a);

    -127
    除了十进制,其他进制计算机存储的都是补码,运算的时候要站在计算机的角度。
    十进制可以站在用户的角度。

数的范围

有符号和无符号的区别:
有符号最高位是符号位,无符号最高位是数的一部分。无符号没有负数

有符号的数的范围:

正数:
0000 0000 ~ 0000 0111
0 ~ 127
负数:
1000 0000 ~ 1111 1111
-0 ~ -127
需要注意的是-0可以当作-128使用

原因:-128的原码和补码是一样的
原码:1 1000 0000
反码:1 0111 1111
补码:1 1000 0000

数值越界:原因计算机存储数据都是以二进制补码的形式来进行存储的
char a=127+2
printf("%d",2); //输出位-127

IDE调试

  1. 设置断点(F9)
  2. 调试运行(F5)
  3. 跳过函数(F10)
  4. 进入函数(F11)

字符数组和字符串的区别

  1. 字符数组是一个普通的数组,字符数组以"\0"或者数字0结尾就是字符串
  2. 打印的时候,普通数组无法一次性打印出来,但是字符数组和字符串可以通过%s一次性打印
  3. 如果字符数组没有结束符数字0,就会出现乱码(烫??烫),这也是常考的考点
  4. 数组名是常量,数组需要在初始化的时候指定占用的空间,否则编译器会报错
  5. C语言中没有字符串类型,用字符数组模拟
  6. 字符串一定是字符数组,但是字符数组不一定是字符串
  7. 字符数组的初始化可以用双引号

数组越界:数组越界,是指访问了超出数组定义的内容

  1. 编译时错误,语法错误
  2. 运行时错误,逻辑错误(编译时不会提示)
  3. VS会提示内存出现问题,卡住。

测试数组大小

sizeof(数组名)

冒泡排序,两个数相比较,大于交换,最大值放在最后

面试经常提问

打印地址

%p

字符0和\0

它们的ASCII码不同,打印会有所不同,'0'的ASCII码是48,\0的ASCII码是0

字符数组的输入

遇到空格截断scanf("%s", str); str=hello world输入的是hello,空格后面的字符放在了缓冲区
scanf()的缺陷,不做越界检查,所以这个安全隐患,VS不让用这个函数,除非加上了宏定义

随机数的产生

种子需要每次都改变,随机数才会发生改变,通常情况此下,用时间作为种子

gets和fgets函数

gets:这个函数已经被抛弃了,它的功能是从键盘读取字符串,放在指定的数组。它不作越界检查,函数不安全。
fgets:从标准输入读取内容,如果输入内容大于sizeof(buf)-1,只取sizeof(buf)-1个字符,放在buf数组,需要注意的是它会把字符也读取进去。

puts和fputs函数

puts:把buf内容输出到屏幕,自动在屏幕加换行,字符串本身没有换行符,是函数本身的。
fputs:和printf一样的效果,只是在文件操作时较为方便。

return和exit的区别

return:中断函数,中断main函数,程序停止
exit:结束整个程序(结束进程)

声明和定义的区别

  1. 在main函数中调用其他函数,只会往前找函数的定义
  2. 函数的声明就是把函数名字复制到main函数之前就可以了。
  3. 如果定义没有放在main函数前面,在调用函数前需要声明,声明加不加extern是一样的。
  4. 函数第一只能有一次,声明可以有多次
  5. 可以不定义只是声明,调用时会出错
  6. 声明的形参可以和定义的形参不同

分文件编程

  1. 按功能来分 xxx.h xxx.c
  2. 有一个主函数测试自定义函数 main.c
  3. 有一个不成文的规定,xx.c文件对应的头文件就是xx.h,文件名字一样的。
  4. 自定义的头文件include用双引号,系统定义的使用尖括号
  5. 在头文件中声明对应的.c文件中的函数,加不加extern关键字作用是一样的。
  6. 不能在头文件中定义函数,可以声明函数。对变量也是一样的。在头文件中定义函数,引用时有可能会出现多次定义的情况。

同一个文件如何防止多次包含:

  1. #pragma once //包含多次只有一次生效

  2. #ifndef _MyFunction

    #define _MyFunction

    #endif

分文件编程不一定是一个函数分一个函数

指针

运行程序,加载到内存,运行这的程序叫做进程

//定义变量,分配内存

每个字节的内存都有标号,这个标号就是地址,也叫指针

地址需要存储,32位编译器用32位(4字节)存此地址。

64位编译器用64位存地址

指针也是一种数据类型

p是一个变量,p的类型是int *,p的地址是&p,p里面存的也是一个地址,它存的是某个变量的地址。

*p等价于*(p+0)等价于p[0]。如果写成p[1]会产生越界错误,这是因为操作了野指针,它等价于操作了*(p+1)的内存,因为p+1这个地址是未知的。

*在定义变量是,它表示类型;在使用变量时,它代表操作指针所指向的内存

段错误:指的是非法操作内存,数组越界或者指针错误

野指针(常考):这个指针变量保存了一个没有意义(非法)的地址

合法地址:只有定义的变量后,这个地址就是合法的。

野指针本身没错,操作了非法地址才会出错。

操作系统中将0-255作为系统占用不允许访问操作。程序中允许存在野指针,使用野指针可能会报错。

不建议将变量的值直接赋值给指针。

空指针是指内存地址编号为0的空间,操作空指针一定会报错。空指针可以用于条件判断

数字0和'\0'和NULL都是一个意思,只不过NULL常用于指针,前两个多用于字符串。

多级指针:在需要保存变量地址的类型基础上加上一个*

万能指针:void不可以定义普通类型的变量,不能确定类型,就不能知道分配多大的空间。

可以定义void* 的变量,void*指针也叫万能指针

void*可以指向任何类型的变量(类型匹配),使用指针所指向的内存时,最好转换为它本身的指针类型(因为要指定明确的操作的内存空间大小)【*(int* p))】里面的int*就是指定了空间大小

上面说的指定的内存空间大小就是指的是指针步长。p+1指的是加的是一个步长,而不是数学上的1。步长由指针指向的数据类型决定。

不做类型转换,无法确定步长

const修饰的指针变量

面试经常问的:

const修饰,代表指针所指向的内存是只读:const int/int const p;

const修饰指针变量,代表指针变量的值为只读:int* const p = &a;

const同时修饰指针和变量,代表只读:const int * const p = &a;

数组和指针

数组名是常量,不允许修改,int* const p

指针数组

它是数组,每个元素都是指针

数组指针

它是指针,指向数组的指针

指针存在的意义

能够通过函数间接的操作内存。

如果想通过函数来改变实参,必须地址传递。

形参中的数组和非形参数组的区别

形参中的数组,不是数组,而是普通指针变量

形参数组:int a[10000],int a[],int a对编译器而言,没有任何区别,编译器都当作int 处理,64位系统中sizeof(a)的结果是8,而非数组的长度,sizeof(a[0])的结果就是4,因为第0个元素是int类型。

非形参的数组就是真的数组了

err,二维数组不是二级指针

函数指针

下面的程序面试会经常问到:

int* fun(){
    int a;
    return &a; //err
}
int main(){
    int* p=NULL;
    p=fun();
    *p=100;//操作了野指针
}

上面的代码在不同的平台运行有的会成功有的不会成功,但是是错误的(VS和Qt通过,Linux未通过)。因为当fun函数执行完毕后,变量a被释放。再次操作a的内存时就是操作了野指针。

linux64位gcc,不允许返回局部变量地址,因为返回后会无意义,会引起操作野指针的错误。

定义的全局变量可以解决上面的错误。

全局变量在任何地方都能使用,全局变量只有在整个程序结束后才释放。全局变量在data区存放

字符指针

%s:打印一个字符串

打印的时候%s会做一个额外的处理,所以会一次行打印出字符串,传入的是一个首元素的地址。

%s占位符,逗号后面对应的是一个地址,计算机会打印这个地址后面的所有字符,直到遇到\0停止打印。

%d,占位符,传入的的是一个变量,打印的也就是这个变量的值。

strcpy是拷贝给p所指向的内存空间拷贝内容,而不是给p拷贝内容。如果没有定义p指向的内存空间,就会出现操作野指针的错误(段错误)。

字符串

以字符串双引号形式初始化数组,自动隐藏结束符'\0'。
如果以单个字符类似数组的方式的赋值,打印%s会出现乱码。

每个字符串都是一个地址,这个地址是指字符串的首元素的地址

字符串常量放在data区,文字常量区。只读,不能修改。(面试经常考)
int* p = "hello"
strcpy(p,"abc")//error 段错误

main函数参数

main函数的参数:argv[]等价于*argv,也可以写成char* *argv

main函数的参数是由用户运行的时候传递的。

如果要指向上一个指针的地址,那就比上一个指针多一个*就可以了

查找匹配字符串出现的次数

strstr()函数的使用

字符串常量和字符指针数组

char* p1 = "abc" //存放在文字常量区的指针变量,不不能修改,可以拷贝到栈区就可以修改了

char p2[] = "abc" //存放在栈区,可以改变数组元素

char p3[100]; p3="abc"//err数组名是常量

内存管理

作用域和生命周期。

需要注意的就是当内存释放了就不要使用了,这是非法操作内存。

static局部变量,在编译阶段就已经分配了空间,函数没有调用前,就已经存在了。只有程序结束,static变量才自动释放。如果不做初始化,默认为0。这个语句只会初始化一次,但是可以赋值多次。

普通局部变量和static局部变量的区别

  1. 内存分配和释放
  2. 初始化

二级指针和字符指针数组

char** p。p是char**类型,指向char*类型

C语言全局变量的缺陷

C语言全局变量多次定义不会出现编译不会报错

int a;
int a;
int a;

上面的代码中有一次是定义,两次是声明。但是不能确定那个是定义。

如果定义一个全局变量,没有赋值(初始化),无法确定是定义还是声明。

如果定义一个全局变量,同时初始化,那么这个就是定义,其他语句是声明。

只有声明,没有定义,无法给变量赋值

因此,定义全局变量的同时,建议初始化,声明一个全局变量,加extern关键字。

全局变量分文件

在main函数里面或者外面声明都可以

使用函数前,声明函数

声明函数,extern可有可无

如果事先为声明,gcc编译会通过,但是会有警告,c++编译器不会通过。因此建议先声明函数,后使用。

声明不需要加头文件

头文件和.c文件

一般来讲头文件声明,.c文件声明不定义(对于同一个文件而言。)在main函数中直接包含头文件

全局变量在任何位置都只能定义一次,不管两个文件有没有关系。

如果在头文件中做定义,就可能会在预处理头文件展开时,会出现重复定义的问题。

普通全局变量和static全局变量的区别

  1. 普通全局变量是在{}外面定义的变量,全局变量所有文件只能有一个。普通全局变量所有文件都能使用。
  2. static全局变量一个文件只能有一个的定义,多个文件可以有多个,static全局变量只能在当前文件使用。
  3. 因此不同文件只能出现一个普通变量的定义
  4. 一个文件只能有一个static全局变量的定义,不同文件间的static全局变量,就算名字相同,也是没有关系的两个变量

面试会问:为了数据安全,把一个数据定义为static全局变量

static:内部链接

普通全局变量:外部链接

普通函数和static函数的区别和变量是一样的

auto变量:作用于一对{}内,声明周期为当前函数

内存分区(内存布局)

写代码一般不会用到,但是面试会可能会问到

size a.out #查看分区

在程序没有执行前,有几个内存分区已经确定,虽然分区确定,但是没有加载内存,程序只有运行时才加载内存。

  • text:(代码区)只读,主要放函数
  • data:初始化的数据,全局变量,static变量,文字常量区(只读)
  • bss:没有初始化的数据,全局变量,static变量

当运行程序,加载内存,首先根据前面确定的内存分区先加载,然后额外加载两个区:

  • stack:(栈区)普通局部变量,自动管理内存,先进后出的结构特点。
  • heap:(堆区)手动申请空间,手动释放,整个程序结束,系统也会自动回收。如果没有手动释放,程序也没有结束,这个堆区空间不会自动释放。

栈越界

栈越界就是栈区空间用光了

语法上没有问题,栈区分配很大的内存,越界了,导致段错误。段错误都是内存错误引起的。

这种错误通常是出现在函数递归的时候。

内存操作函数

和strcpy, strncpy, strcmp类似,但是如果是字符串,遇到\0结束是无法解决的。但是内存操作函数可以解决这个问题。

memset(&a, 97, sizeof(a));

其中97是以字符的ascii码来对a进行赋值的。主要用来清0数组

这个操作的第二个参数是针对字符来用的,不是整数。

其他函数:memcpy, memmove, memcmp

内存重叠

如果出现内存重叠,最好使用memmove代替memcpy

指针指向栈区空间

定义一个变量,指针指向该变量地址即可

指针指向堆区空间

使用malloc函数在堆区申请一个空间

参数指定堆区分配多大的空间malloc( sizeof(int) )

返回值,成功就是堆区空间的首元素地址,失败返回NULL

注意返回的地址是void*类型的,需要类型转换为int*,如果不转换,gcc编译可以通过,但是c++就会失败。

释放空间用free(p),不是释放p变量,而是释放p所指向的内存

同一块堆区空间只能释放一次

所谓的释放不是内存消失,而是指这块内存用户不能再使用,系统回收了,如果用户再次使用,就是使用野指针。

内存泄漏

动态分配了空间,不释放

int* p = NULL

.....

if(NULL != p)
{
    free(p);
    p = NULL;
}

内存污染

非法操作内存:操作野指针所指向内存,堆区越界

下面是错误代码,没有分配空间,但是却gcc编译成功。vs运行程序会卡死

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

int main(int argc, char *agrv[]){
    
    char *p = NULL;
    
    p = (char *)malloc(0); //没有分配空间
    if(p==NULL){
        printf("分配失败\n");
        return 0;
    }
    
    strcpy(p, "helloworld");
    printf("%s\n", p);
    
    free(p);
    
    return 0;
    
}

堆区越界

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

int main(int argc, char *agrv[]){
    
    char *p = NULL;

    p = (char *)malloc(0); //没有分配空间
    if(p==NULL){
        printf("分配失败\n");
        return 0;
    }

    strcpy(p, "helloworld");
    printf("%s\n", p);

    free(p);

    return 0;
}

为什么指针指向首元素就可以遍历整个数组

因为数组在内存中的存储是连续的

Java和C++封装了内存释放函数,用户只需要使用,但是C语言没有这种封装,所以会涉及到的内存问题较多。

堆区数组

p = (int * )malloc( 10 * sizeof(int) )

相当于在堆区开辟了类似栈区数组 int arr[10]; int *p = arr;

内存分区分析

值传递:

void fun(int *tmp){ //tmp存储在栈区空间
    tmp = (int *)malloc(sizeof(int) ); //操作堆区空间的内存
    *tmp = 100;
}

int main(int argc, char *argv[]){
    int *p = NULL;
    func(p); //值传递,形参修改不会影响实参
    printf("*p = %d\n", *p); //err,操作空指针
}

结构体

一个结构体变量就是C++中的一个对象

关键字 struct

struct Student{
    ;
}; //有分号,另一个就是do while了,两个特殊例子

struct Student合起来才是结构体类型名字

结构体内部定义的变量不能直接赋值

结构体只是一个类型,没有定义变量前,是没有分配空间的,没有分配空间,就不能赋值。

struct Student stu;//定义变量

结构体变量初始化,和数组一样,要使用大括号,而且只有在定义时才能初始化。

struct Student stu2 = {10, "mike", 40};

如果是普通变量,使用点运算符赋值。

如果是指针变量,使用->运算符;如果是指针,指针有合法指向,才能操作结构体成员。

任何结构体变量都可以用点或者箭头操作成员

猜你喜欢

转载自www.cnblogs.com/thethomason/p/9831713.html
今日推荐