嵌入式之C语言

一、数据类型

char      //字符数据类型    //1字节
short     //短整型         //4字节
int       //整形           //4字节
long      //长整型         //4字节
long long //更长的整形     //8字节
float     //单精度浮点数    //4字节
double    //双精度浮点数    //8字节

二、变量的作用域和生命周期

作用域 

作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的而限定这个名字的可用性的代码范围就是这个名字的作用域。

1. 局部变量的作用域是变量所在的局部范围。
2. 全局变量的作用域是整个工程。 

生命周期 

变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段

1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
2. 全局变量的生命周期是:整个程序的生命周期。

三、常量

C语言中的常量分为以下以下几种:

  • 字面常量
  • const 修饰的常变量
  • #define 定义的标识符常量
  • 枚举常量
#include <stdio.h>
//举例
enum Sex
{
    MALE,
    FEMALE,
    SECRET
};
//括号中的MALE,FEMALE,SECRET是枚举常量
int main()
{
    //字面常量演示
    3.14;//字面常量
    1000;//字面常量
    //const 修饰的常变量
    const float pai = 3.14f; //这里的pai是const修饰的常变量
    pai = 5.14;//是不能直接修改的!
    //#define的标识符常量 演示
    #define MAX 100
    printf("max = %d\n", MAX);
    //枚举常量演示
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);
    //注:枚举常量的默认是从0开始,依次向下递增1的
    return 0;
}

 注:上面例子上的 pai 被称为 const 修饰的常变量, const 修饰的常变量在C语言中只是在语法层面限制了变量 pai 不能直接被改变,但是 pai 本质上还是一个变量的,所以叫常变量。

四、字符串

#include <stdio.h>
//下面代码,打印结果是什么?为什么?(突出'\0'的重要性)
int main()
{
    char arr1[] = "bit";
    char arr2[] = {'b', 'i', 't'};
    char arr3[] = {'b', 'i', 't', '\0'};
    printf("%s\n", arr1);
    printf("%s\n", arr2);
    printf("%s\n", arr3);
    return 0;
}

运行结果: bit、bit+随机值、bit

注:字符串的结束标志是一个 \0 的转义字符,相当于null。在计算字符串长度的时候 \0 是结束标志,不算作字符串内容。 

(一)数据输入函数

1.getchar:从终端输入一个字符

函数原型:int getchar(void);
返回值:成功返回对应字符的ASCII码,失败或结束返回EOF(-1)

注:getchar 会直接读取 '\n' 和空格,所以只读取一个字符不会造成脏字符 

2.scanf:格式化输入

函数原型:int scanf(const char *format, ...);
const char *format:格式化的控制串,要输入的数据的类型
...:输入的数据存放的空间地址
返回值:成功是成功输入数据的个数,失败0, 错误-1
注意输入结束的标志:' ', '\t', '\n'

解决脏字符('\n'):

  1.     %*c
  2.     getchar() 

 注: 对于 scanf 函数,'\n' 会触发 scanf 读取输入缓冲区的内容,但遇到 '\n' 或空格 ' ' 会停止读取,所以会造成“脏字符”。

3.gets:通过字符串地址读取

函数原型:char *gets(char *str);

gets函数用于从标准输入(键盘)读取一行字符串,并将其存储到指定的字符数组中

注: 

  1.  gets函数会读取整行输入,包括空格和制表符等字符,直到遇到换行符为止。它会将换行符(\n)也包含在读取的字符串中。如果要去掉换行符,可以在读取字符串后使用strchrstrlen函数来处理。
  2. gets函数不会检查输入字符串是否超过了目标数组的大小,这可能导致缓冲区溢出的安全问题。因此,建议使用fgets函数替代gets函数来读取字符串,因为fgets函数可以指定最大输入字符数,并且会自动处理换行符。
  3. 使用 gets() 时,系统会将最后“敲”的换行符从缓冲区中取出来,然后丢弃,所以缓冲区中不会遗留换行符。这就意味着,如果前面使用过 gets(),而后面又要从键盘给字符变量赋值的话就不需要吸收回车清空缓冲区了,因为缓冲区的回车已经被 gets() 取出来扔掉了。

(二)数据输出函数

1.putchar:输出单个字符

函数原型: int putchar(int c);
参数:要输出的字符或字符变量
返回值:成功返回输出字符的ASCII码,失败返回EOF(-1)

2.printf:格式化输出数据

函数原型:int printf(const char *format, ...); 
printf是一个不定参数的函数
const char *format:格式化化输出的字符串(1、%[修饰符]格式符 指定输出格式;2、普通字符原样输出)
...:指定输出的数据

3.puts:通过字符串地址输出

函数原型:int puts(const char *str);

puts函数用于将字符串输出到标准输出(屏幕)

puts函数接受一个字符串参数str,并将该字符串输出到屏幕上。输出的字符串会自动添加换行符(\n)。puts函数会返回一个非负整数,表示成功输出的字符数(不包括结尾的换行符)。如果输出失败,则返回EOF(-1)。

(三)字符串输入脏字符

示例一 

#include<stdio.h>
int main()
{
	//我们输入一个数和一个字符
	int height;
	char id;
	scanf("%d", &height);
 
	scanf("%c", &id);
	printf("%d %c", height, id);
	return 0;
}

上述代码的输出为: 

当我们输入数字1111并按下回车之后,系统会自动的吧我们所按的回车当作字符存在id中,导致了运行错误!!(这是最简单的理解吧)

改进为:

#include<stdio.h>
int main()
{
	//我们输入一个数和一个字符
	int height;
	char id;
	scanf("%d", &height);
	getchar();//读入回车,清空缓冲区
	scanf("%c", &id);
	printf("%d %c", height, id);
	return 0;
}

 正确的运行结果为:

示例二 

#include<stdio.h>
int main()
{
	char arr[100] = { 0 };
	gets(arr);
	printf("%s", arr);
	return 0;
}

 这里它读入了空格并且打印

 总结:

从标准输入设备(如键盘)读取字符到s所指向的数组中,直到读到文件末尾或者换行符‘\n’。换行符被丢弃,最后一个字符读入后写入一个 ‘\0’。若成功则返回s,若无字符读入数组或者读取失败返回空指针NULL。

示例三

#include<stdio.h>
int main()
{
	char arr[100] = { 0 };
	scanf("%s", arr);
//注意字符数组的数组名即地址,不用取址符&
	printf("%s", arr);
	return 0;
}

这里我输入了:i love you 但是它只打印了 i 

我输入了:   hello(前面有三个空格)但是空格也没有打印 

 总结: scanf函数读取用户键入的字符到字符数组,直到遇到空格,回车,或者文件结束符(EOF)为止,空格,回车,或者文件结束符被丢弃,最后一个字符读入后往字符数组中写入结束符'\0'。

(四)字符函数和字符串函数

求字符串长度:strlen 

size_t strlen ( const char * str );  

  1. 字符串已经 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包'\0' )
  2. 参数指向的字符串必须要以 '\0' 结束。
  3. 注意函数的返回值为size_t,是无符号的( 易错

 模拟实现strlen(三种方式)

 方式一: 

//计数器方式
int my_strlen(const char * str)
{
     int count = 0;
     while(*str)
     {
         count++;
         str++;
     }
     return count;
}

 方式二:

//不能创建临时变量计数器
int my_strlen(const char * str)
{
     if(*str == '\0')
         return 0;
     else
         return 1+my_strlen(str+1);
}

 方式三: 

//指针-指针的方式
int my_strlen(char *s)
{
    char *p = s;
    while(*p != ‘\0’ )
        p++;
    return p-s;
}

长度不受限制的字符串函数

1、strcpy 

 char* strcpy(char * destination, const char * source ); 

  1. 源字符串必须以 '\0' 结束。
  2. 会将源字符串中的 '\0' 拷贝到目标空间。
  3. 目标空间必须足够大,以确保能存放源字符串。
  4. 目标空间必须可变。

 模拟实现strcpy

//1.参数顺序
//2.函数的功能,停止条件
//3.assert
//4.const修饰指针
//5.函数返回值
//6.题目出自《高质量C/C++编程》书籍最后的试题部分
char *my_strcpy(char *dest, const char*src)
{ 
    char *ret = dest;
    assert(dest != NULL);
    assert(src != NULL);
 
     while((*dest++ = *src++))
     {
         ;
     }
     return ret;
}
2、strcat

char * strcat ( char * destination, const char * source );

  1. 源字符串必须以 '\0' 结束。
  2. 目标空间必须有足够的大,能容纳下源字符串的内容。
  3. 目标空间必须可修改。

模拟实现strcat 

char *my_strcat(char *dest, const char*src)
{
     char *ret = dest;
     assert(dest != NULL);
     assert(src != NULL);
     while(*dest)
     {
         dest++;
     }
     while((*dest++ = *src++))
     {
         ;
     }
     return ret;
}
3、strcmp

int strcmp ( const char * str1, const char * str2 );  

  1. 第一个字符串大于第二个字符串,则返回大于0的数字
  2. 第一个字符串等于第二个字符串,则返回0
  3. 第一个字符串小于第二个字符串,则返回小于0的数字

 模拟实现strcmp

int my_strcmp (const char * src, const char * dst)
{
    int ret = 0 ;
    assert(src != NULL);
    assert(dest != NULL);
    while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst)
        ++src, ++dst;

    if ( ret < 0 )
        ret = -1 ;
    else if ( ret > 0 )
        ret = 1 ;
    return( ret );
}

长度受限制的字符串函数

1、strncpy

char * strncpy ( char * destination, const char * source, size_t num );  

  1. 将源的第一个字符数复制到目标。如果源 C 字符串的末尾(由空字符表示)在复制 num 字符之前找到,目标用零填充,直到写入总共 num 个字符
  2. 拷贝num个字符从源字符串到目标空间
  3. 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个  
2、strncat

char * strncat ( char * destination, const char * source, size_t num );

  1. 将源的第一个数字字符附加到目标,加上终止空字符
  2. 如果源中 C 字符串的长度小于 num,则只有终止之前的内容复制空字符
3、strncmp

int strncmp ( const char * str1, const char * str2, size_t num );

  1. 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完 

字符串查找

1、strstr

char * strstr ( const char *str1, const char * str2);

  1. 返回指向 str1 中第一次出现的 str2 的指针,如果 str2 不是 str1  

模拟实现strstr 

char *  strstr (const char * str1, const char * str2)
{
    char *cp = (char *) str1;
    char *s1, *s2;
    if ( !*str2 )
        return((char *)str1);
    while (*cp)
    {
        s1 = cp;
        s2 = (char *) str2;
        while ( *s1 && *s2 && !(*s1-*s2) )
            s1++, s2++;
            if (!*s2)
                return(cp);
            cp++;
     }
     return(NULL);
}
2、strtok

char * strtok ( char * str, const char * sep );

  1. sep参数是个字符串,定义了用作分隔符的字符集合
  2. 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记  
  3. strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:stock函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容 并且可修改。) 
  4. strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置
  5. strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记
  6. 如果字符串中不存在更多的标记,则返回 NULL 指针

内存操作函数 

1、memcpy

void * memcpy ( void * destination, const void * source, size_t num );  

  1. 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  2. 这个函数在遇到 '\0' 的时候并不会停下来。
  3. 如果source和destination有任何的重叠,复制的结果都是未定义的。 

模拟实现memcpy 

void * memcpy ( void * dst, const void * src, size_t count)
{
    void * ret = dst;
    assert(dst);
    assert(src);
    /*
     * copy from lower addresses to higher addresses
     */
    while (count--) {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
       }
    return(ret);
}
2、memmove

void * memmove ( void * destination, const void * source, size_t num );

  1. 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  2. 如果源空间和目标空间出现重叠,就得使用memmove函数处理。 

模拟实现memmove 

void * memmove ( void * dst, const void * src, size_t count)
{
    void * ret = dst;
    if (dst <= src || (char *)dst >= ((char *)src + count)) {
/*
 * Non-Overlapping Buffers
 * copy from lower addresses to higher addresses
 */
        while (count--) {
            *(char *)dst = *(char *)src;
            dst = (char *)dst + 1;
            src = (char *)src + 1;
        }
    }
    else {
 /*
  * Overlapping Buffers
  * copy from higher addresses to lower addresses
  */

        dst = (char *)dst + count - 1;
        src = (char *)src + count - 1;
        while (count--) {
            *(char *)dst = *(char *)src;
            dst = (char *)dst - 1;
            src = (char *)src - 1;
        }
    }
    return(ret);
}
3、memset
4、memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num ); 

  1. 比较从ptr1和ptr2指针开始的num个字节 

 五、转义字符

转义字符释义
\?在书写连续多个问号时使用,防止他们被解析成三字母词
\'用于表示字符常量'
\"用于表示一个字符串内部的双引号
\\用于表示一个反斜杠,防止它被解释为一个转义序列符。
\a警告字符,蜂鸣
\b退格符
\f进纸符
\n换行
\r回车
\t水平制表符
\v垂直制表符
\dddd d d表示1~3个八进制的数字。 如: \130 表示字符X
\xddd d表示2个十六进制数字。 如: \x30 表示字符0

注:一个转义字符是1个字节 

 笔试题:

//程序输出什么?
#include <stdio.h>
int main()
{
    printf("%d\n", strlen("abcdef"));
    // \62被解析成一个转义字符
    printf("%d\n", strlen("c:\test\628\test.c"));
    return 0;
}

运行结果:6,14

六、ASCII码表 

七、常见关键字

auto     break     case     char     const     continue     default     do     double     else     enum
extern     float     for     goto     if     int     long     register     return     short     signed
sizeof     static     struct     switch     typedef     union     unsigned     void     volatile     while 

(一)关键字typedef 

typedef 顾名思义是类型定义 ,这里应理解为类型重命名

//将unsigned int 重命名为uint_32, 所以uint_32也是一个类型名
typedef unsigned int uint_32;
int main()
{
    //观察num1和num2,这两个变量的类型是一样的
    unsigned int num1 = 0;
    uint_32 num2 = 0;
    return 0;
}

(二)关键字static

在c语言中:static是用来修饰变量和函数的

  1. 修饰局部变量-称为静态局部变量
  2. 修饰全局变量-称为静态全局变量
  3.  修饰函数-称为静态函数

 修饰局部变量(相当于全局)

//代码1
#include <stdio.h>
void test()
{
    int i = 0;
    i++;
    printf("%d ", i);//1 1 1 1 1 1 1 1 1 1
}
int main()
{
    int i = 0;
    for(i=0; i<10; i++)
    {
        test();
    }
    return 0;
}
//代码2
#include <stdio.h>
void test()
{
    //static修饰局部变量
    static int i = 0;
    i++;
    printf("%d ", i);//1 2 3 4 5 6 7 8 9 10
}
    int main()
{
    int i = 10;
    for(i=0; i<10; i++)
    {
        test();
    }
    return 0;
}

结论:static修饰局部变量改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。 

修饰全局变量 

//代码1
//add.c
int g_val = 2018;
int main()
{
    return 0;
} 
//引入add.c的全局变量
extern int g_val;
//test.c
int main()
{
    printf("%d\n", g_val);
    return 0;
}

//代码2(加了static修饰)
//add.c
static int g_val = 2018;
//test.c
int main()
{
    printf("%d\n", g_val);
    return 0;
}
//引入add.c的全局变量
extern int g_val;
//test.c
int main()
{
    printf("%d\n", g_val);
    return 0;
}

运行结果:代码1正常,代码2在编译时会出现连接性错误。 

结论:一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。 

 修饰函数

//代码1
//add.c
int Add(int x, int y)
{
    return x+y;
}
//test.c
extern int Add(int,int);//声明函数
int main()
{
    printf("%d\n", Add(2, 3));
    return 0;
}

//代码2
//add.c
static int Add(int x, int y)
{
    return x+y;
}
//test.c
extern int Add(int,int);//声明函数
int main()
{
    printf("%d\n", Add(2, 3));
    return 0;
}

运行结果:代码1正常,代码2在编译时会出现连接性错误。  

结论:一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。  

 (三)关键字const

我们知道const修饰变量,这个变量就被称为常变量,不能被修改,但本质上还是变量 

#include<stdio.h>
int main()
{
    const int num = 10;
    num = 20;//报错erro,const修饰不能被修改

    int *p = &num;
    *p = 20;//可以修改。怎么解决呢???
    return 0;
}

解决方法:在指针变量前加const

#include<stdio.h>
int main()
{
    const int num = 10;
    num = 20;//报错erro,const修饰不能被修改

    const int *p = &num;
    *p = 20;//报错erro,const修饰指针表示指针指向的内容,是不能通过指针来改变的
    return 0;
}

区分const放在*前和*后的含义

#include<stdio.h>
int main()
{
    const int num = 10;
    int *const p = &num;//const放在*右边,修饰的是指针变量p,
                        //表示指针变量不能被改变但是指针的内容,可以被改变
    int n = 20;
    *p = 20;
    p = &n;//报错erro 
    return 0;
}

总结:const修饰指针变量时

1.const放在*的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的,但是指针变量本身是可以修改的

2.const放在*的右边,修饰的是指针变量p,表示指针变量不能改变,但是指针指向的内容,可以改变

八、#define定义常量和宏 

//define定义标识符常量

#define MAX 1000

//define定义宏

#define ADD(x, y) ((x)+(y))
#include <stdio.h>

int main()
{
    int sum = ADD(2, 3);
    printf("sum = %d\n", sum);
    
    sum = 10*ADD(2, 3);
    printf("sum = %d\n", sum);
    
    return 0;
}

注:#define ADD(x,y)  ((x)+(y))里的((x)+(y))x和y都加了括号,原因是不能想象成一个普通变量,而是一个表达式 

细节:如下 

注:定义宏的本质相当于替换 

九、数组 

说明:全局数组未初始化,值默认为0;局部数组未初始化,默认为随机数

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

int arr1[5];

int main(int argc, const char *argv[])
{
    int arr2[5];
    printf("arr1:\n");
    for(int i=0;i<5;i++){
        printf("%d ",arr1[i]);
    }

    puts("");

    printf("arr2:\n");
    for(int i=0;i<5;i++){
        printf("%d ",arr2[i]);
    }
 
	return 0;
}

 结果:
​​​​​​​

(一)一维数组

一维数组的创建与初始化 

char arr[5];
strlen(arr)//未初始化:随机值
sizeof(arr)//容量[]里有多少就是多少:5

char arr1[] = {'a','b','c'};
strlen(arr1)//未初始化容量[],所以是无限容量:随机值
sizeof(arr1)//未初始化容量[],{}有多少就是多少:3

char arr2[] = "abcdef";//后面加了'\0',"abcdef'\0'",相当于{'a','b','c','d','e','\0'}
strlen(arr2)//6
sizeof(arr2)//后面省略了'\0',容量+1:

char arr3[5] = {'a','b','c'};
strlen(arr3)//3
sizeof(arr3)//5

char arr4[5] = {'a','b','c','d','e'};
strlen(arr4)//5
sizeof(arr4)//5

int arr5[5];
sizeof(arr5)//随机值

int arr6[] = {1,2,3,4};//相当于{1,2,3,4,0,0,0,...}
sizeof(arr6)//4

int arr7[5] = {1,2,3};//相当于{1,2,3,0,0}
sizeof(arr7)//5

int arr8[5] = {1,2,3,4,5};
sizeof(arr8)//5

一维数组在内存中的存储

#include <stdio.h>

int main()
{
 int arr[10] = {0};
 int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    
 for(i=0; i<sz; i++)
 {
 printf("&arr[%d] = %p\n", i, &arr[i]);
 }
 return 0;
}

结果: 

 总结:int类型数组中第一个地址+4等到下个地址,因为int占4个字节

 数组取地址细节问题

#include<stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%p",arr);//地址值:0x7ffe5648cc30,
                     //打印首元素地址,类型:int *p(变量指针),
                     //arr+1表示下一个元素地址(0x7ffe5648cc30+4),地址值:0x7ffe5648cc38

    printf("%d",&arr[0]);//地址值:0x7ffe5648cc30,
                         //打印首元素地址,类型:int *p(变量指针)
                         //&arr+1[0]+1表示下一个元素地址(0x7ffe5648cc30+4),地址值:                        
                         //0x7ffe5648cc38

    printf("%p",&arr);//地址值:0x7ffe5648cc30,
                      //打印数组地址,类型:int (*p)[10](数组指针),
                      //&arr+1表示跳过整个数组的空间,指向数组末尾后面的一个内存地址或者下个数组        
                      //地址
    return 0;
}

总结:arr,&arr[0],&arr地址值一样但表示含义不同。arr和&arr[0]都表示数组首元素,类型都是变量指针;&arr表示数组的地址,类型是数组指针

数组取地址细节问题补充 

int arr[10] = {0};

printf("%p",arr);//arr:首元素地址
printf("%d\n", sizeof(arr));//arr:整个数组。输出结果:40

既然arr为首元素地址,为什么输出的结果是:40? 

结论:数组名是数组首元素的地址。(有两个例外)

1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数 组。

2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。 

 除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。

(二)二维数组

 二维数组的创建与初始化

//数组创建
int arr[3][4];

char arr[3][5];

double arr[2][4];
//数组初始化

int arr[3][4] = {1,2,3,4};

int arr[3][4] = {{1,2},{4,5}};

int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略

二维数组在内存中的存储 

#include <stdio.h>

int main()
{
     int arr[3][4];
     int i = 0;
     for(i=0; i<3; i++)
    {
         int j = 0;
         for(j=0; j<4; j++)
        {
             printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
        }
    }
 return 0;
}

结果: 

十、操作符 

 (一)操作符分类

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员

(二)算数操作符 

++和--实例 

int i = 10;
printf("%d %d %d %d %d\n", i--, ++i, --i, i, i++);

代码的结果是什么?(在GCC编译器下)(易错) 

运行结果:11 10 10 10 10 

技巧:前++及前--还有本身结果是所有表达式最终的结果。
            后++及后--的结果是参数表达式从右往左依次计算时的值。

注意:该技巧也适用于函数

(三)移位操作符

<< 左移操作符

>> 右移操作符

  

注:移位操作符的操作数只能是整数。 

须知的知识点 

内存中存的数是它的补码

整数的二进制表示形式:有3种

  1. 原码:直接根据数值写出的二进制序列就是原码
  2. 反码:原码的符号位不变,其它位按位取反就是反码
  3. 补码:反码+1,就是补码

正数的原码、反码、补码都一样 

左移操作符

移位规则:

左边抛弃、右边补0

 右移操作符 

移位规则: 首先右移运算分两种:

1. 逻辑移位 左边用0填充,右边丢弃

2. 算术移位 左边用原该值的符号位填充,右边丢弃

无符号数左移:高位不管,低位补0
unsigned char a = 6, b;
b = a << 2;
b = 0000 0110 << 2 
b = 0001 1000
无符号数右移:高位补0,低位不管
unsigned char a = 6, b;
b = a >> 2;
b = 0000 0110 >> 2 
b = 0000 0001
有符号数左移:符号位不变,低位补0
char a = -8, b;
b = a << 3;
b = 1111 1000 << 3;    //1111 1000 是-8的补码
b = 1100 0000 		  //移位的结果
b = 1100 0000		 //存储数据的原码
b = -64
有符号数右移:高位补符号位
char a = -8, b;
b = a >> 2;
b = 1111 1000 >> 2;   //1111 1000 是-8的补码
b = 1111 1110		  //移位的结果
b = 1000 0010
b = -2

 总结:无符号数左移:高位不管,低位补0

            无符号数右移:  高位补0,低位不管

            有符号数左移:  符号位不变,低位补0

            有符号数右移:  高位补符号位

(四)位操作符 

& //按位与(同为1才为1,其他都为0)

| //按位或(有1就是1,其他都为0)

^ //按位异或(不同为1,相同为0)

注:他们的操作数必须是整数。

(五)逻辑操作符

&&     逻辑与

||        逻辑或

区分逻辑与和按位与

区分逻辑或和按位或

(六)逗号表达式

 exp1, exp2, exp3, …expN

逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?//13

//代码2
if (a =b + 1, c=a / 2, d > 0)//if(d>0)

//代码3
a = get_val();
count_val(a);
while (a > 0)
{
    //业务处理
    a = get_val();
    count_val(a);
}

如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
         //业务处理
}

逗号表达式陷阱 

int x=10; 
int y=x++; 
printf("%d,%d",(x++,y),y++);//结果是:11,10

结果是:11,10 

总结:printf函数从右至左运算

int a=1,b=2; 
printf("%d\n",a=a+1,a+6,b+2);//结果是2

结果是:2 

 总结:只有1个%d,只打印1个整数,所以后面a+6,a+2省略。相当于printf("%d\n",a=a+1);

int a=1,b=2; 
printf("%d\n",a=a+1,a=a+6,b+2);//结果是8

 结果是:8

总结: 只有1个%d,只打印1个整数,后面a=a+6,a+2省略,但是要先计算之后再省略

(七)整型提升

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

 整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。

#include<stdio.h>
int main()
{
    char a = 3;
    00000000 00000000 00000000 00000011(原码)
    00000000 00000000 00000000 00000011(反码)
    00000000 00000000 00000000 00000011(补码)
---------->00000011//截断
    //1字节,00000011
    //整型提升,00000000 00000000 00000000 00000011
    
    char b = 127;
    //1字节,01111111
    //整型提升,00000000 00000000 00000000 01111111

    char c = a + b; 
    //00000000 00000000 00000000 00000011 + 00000000 00000000 00000000 01111111
    //00000000 00000000 00000000 10000010
    //因为char c 1个字节,所以要截断得到:10000010

    printf("%d",c);//%d打印整形,所以要整形提升(根据符号位提升)
    //10000010 --> 11111111 11111111 11111111 10000010(补码)
    //11111111 11111111 11111111 10000001(反码)
    //10000000 00000000 00000000 01111110(原码)
    //所以结果是-126
    return 0;
}

a和b的值被提升为普通整型,然后再执行加法运算。

加法运算完成之后,结果将被截断,然后再存储于c中。

如何进行整体提升呢?

整形提升是按照变量的数据类型的符号位来提升的

//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
10000000 00000000 00000000 00000001(原码)
11111111 11111111 11111111 11111110(反码)
11111111 11111111 11111111 11111111(补码)   
------>   11111111//截断生成 
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111

//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0

整形提升的例子 

//实例一
int main()
{
    char a = 0xb6;
    short b = 0xb600;
    int c = 0xb6000000;
    if(a==0xb6)//a要整型提升所以是false
        printf("a");
    if(b==0xb600)//b要整型提升所以是false
        printf("b");
    if(c==0xb6000000)//不需要整型提升
        printf("c");
    return 0;
}
    
//实例2
int main()
{
    char c = 1;
    printf("%u\n", sizeof(c));//1
    printf("%u\n", sizeof(+c));//4
    printf("%u\n", sizeof(-c));//4
    return 0;
}

c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字 节.

表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.

(八)算术转换 

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double

double

float

unsigned long int

long int

unsigned int

int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

警告: 但是算术转换要合理,要不然会有一些潜在的问题。

正确示例:
float f = 3.14;
double d = f;

错误示例:
float f = 3.14;
int num = f;//隐式转换,会有精度丢失(相当于强制类型转换)

十一、数据的存储 

(一)数据类型的基本介绍

前面我们已经了解了基本的内置类型:

char               //字符数据类型

short              //短整型

int                  //整形

long               //长整型

long long       //更长的整形

float               //单精度浮点数

double           //双精度浮点数

类型的基本归类

整形家族

char

        unsigned char

        signed char

short

        unsigned short int ]

        signed short int ]

int

        unsigned int

        signed int

long

        unsigned long int ]

        signed long int ]

浮点数家族 

float

double

构造类型

>   数组类型

>   结构体类型    struct

>   枚举类型    enum

>   联合类型    union

指针类型 

int *pi;

char *pc;

float* pf;

void* pv;

空类型 

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型。 

(二)整形在内存中的存储 

数据在内存中以二进制的形式储存,即原码、反码、补码

对于整数来说:

正整数: 原、反、补都相同

负整数 :

原码

直接将数值按照正负数的形式翻译成二进制就可以得到原码。

反码

将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码

反码+1就得到补码。

对于整形来说:数据存放内存中其实存放的是补码 

 为什么呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统 一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程 是相同的,不需要额外的硬件电路。  

(三)大小端介绍

什么大端小端: 

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址 中;

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地 址中。  

为什么有大端和小端:  

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元 都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因 此就导致了大端存储模式和小端存储模式。

例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为 高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高 地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则 为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式 还是小端模式。

设计一个小程序来判断当前机器的字节序(是大端还是小端)。 

//代码1
#include <stdio.h>
int check_sys()
{
     int i = 1;
     return (*(char *)&i);
}
int main()
{
     int ret = check_sys();
     if(ret == 1)
     {
         printf("小端\n");
     }
     else
     {
         printf("大端\n");
     }
     return 0;
}

//代码2
int check_sys()
{
     union
     {
         int i;
         char c;
     }un;
     un.i = 1;
     return un.c;
}

(四)浮点数在内存中的存储       

详细解读: 根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

  • (-1)^S * M * 2^E
  • (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
  • M表示有效数字,大于等于1,小于2。
  • 2^E表示指数位。

举例来说:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出S=0,M=1.01,E=2。 十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。

IEEE 754规定:

对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。  

 IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位 浮点数为例,留给M只有23位, 将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。 首先,E为一个无符号整数(unsigned int)

这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们 知道,科学计数法中的E是可以出 现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数 是127;对于11位的E,这个中间 数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

然后,指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。

比如:

0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为

1.0*2^(-1),其阶码为-1+127=126,表示为

01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进 制表示形式为: 

0 01111110 00000000000000000000000  

E全为0 

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

E全为1 

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);  

解释前面的题目:  

下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000 ? 首先,将 0x00000009 拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 , 最后23位的有效数字M=000 0000 0000 0000 0000 1001。  

 →   0000 0000 0000 0000 0000 0000 0000 1001 

由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:    V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)

显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

 再看例题的第二部分。 请问浮点数9.0,如何用二进制表示?还原成十进制又是多少? 首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。

 9.0      1001.0      (-1)^01.0012^3      s=0, M=1.001,E=3+127=130

那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130, 即10000010。

所以,写成二进制形式,应该是s+E+M,即  

0 10000010 001 0000 0000 0000 0000 0000 

这个32位的二进制数,还原成十进制,正是 1091567616 。 

十二、指针

(一)内存 

内存 :每个内存单元大小是1个字节,为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

 

 (二)内存分配

在C语言中,内存可以分为三部分:栈、堆、全局/静态内存

  1.  栈:存放函数内部定义的变量,比如局部变量、函数参数等。栈分配内存由系统自动完成,由编译器在函数调用时进行分配和释放。栈内存是自动管理的,一旦函数执行结束,栈上的内存会自动释放。
  2. 堆:存放动态分配内存空间,由程序员手动使用malloc、calloc等函数分配,手动使用free函数释放。堆内存是通过malloc等动态分配函数手动管理的,如果程序员只分配了内存却没有释放,就会最终导致内存泄漏。
  3. 全局/静态内存:存放全局变量、常量、静态变量等程序静态分配的变量的内存区域,由编译器自动完成内存分配。全局和静态内存与代码编写的位置无关,它们都是在程序运行之前分配好的,并且在程序运行期间一直存在。全局/静态内存可以用在整个程序中,不用担心分配和释放内存,但需要注意内存占用问题。

(三)定义指针与存储地址

int main()
{
    int num = 10;
    int *p;//定义整形指针
    p = &num;//取num的地址并存放在指针p中
    *p = 20;//解引用,通过解析num的地址,改变num里的值
    return 0;
}

(四)指针变量的大小 

#include<stdio.h>
//指针变量大小取决于地址的大小
//32位平台的地址是32个bit(即4个字节)
//64位平台的地址是64个bit(即8个字节)
int main()
{
    printf("%d\n",sizeof(char *));//4或8
    printf("%d\n",sizeof(short *));//4或8
    printf("%d\n",sizeof(int *));//4或8
    printf("%d\n",sizeof(double *));//4或8
    return 0;
}

 结论:指针大小在32位平台是4个字节,64位平台是8个字节。

(五)指针类型的意义 

#include<stdio.h>
int main()
{
    int a = 0x11223344;
    char *pc = (char*)&a;//内存中存储的是44332211
    *pc = 0;//44332211 --> 00332211,只改了一个字节
    return 0;
}
#include <stdio.h>
int main()
{
    int n = 10;
    char *pc = (char*)&n;
    int *pi = &n;
 
    printf("%p\n", &n);
    printf("%p\n", pc);
    printf("%p\n", pc+1);
    printf("%p\n", pi);
    printf("%p\n", pi+1);
    return  0;
}

运行结果: 

总结:指针类型的意义:

1.指针类型决定了:指针解引用的权限有多大

2.指针类型决定了:指针走一步,能走多远(步长) 

(六)一些符号的特殊关系

*、& 、[]:  

*与&互为逆运算:当*与&同时存在时可以相互抵消
             int a[5] = {1,2,3,4,5};
             *&a[0] == a[0]
*与[]等价:*与[]可以相互转换
             int a[5] = {1,2,3,4,5}, *p = a;
             a[0] == *p;         -->*(a+0) == p[0];
             a[1] == *(p+1);     -->*(a+1) == p[1];
             printf("%d %d %d %d\n", a[2], *(a+2), *(p+2), p[2]); //3 3 3
[]与&互为逆运算: 当[]与&同时存在时可以相互抵消
             int a[5] = {1,2,3,4,5};
          int *p = &a[0];  //int *p = a;
          int *q = &a[4];  //int *q = a+4;

总结:当指针px指向数组a的首元素时(0<=i<a的元素个数),那么如下等式恒成立:

a[i] == *(a+i) == *(px+i) == px[i] 

(七)野指针 

原因 

1. 指针未初始化

#include <stdio.h>
int main()
{ 
    int *p;//局部变量指针未初始化,默认为随机值(相当于没有在内存中申请)
    *p = 20;
    return 0;
}

 2. 指针越界访问

#include <stdio.h>

int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}

3. 指针指向的空间释放
这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

如何规避野指针 

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放,及时置NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性

#include <stdio.h>
int main()
{
    int *p = NULL;//初始化为null
    //....
    int a = 10;
    p = &a;
    if(p != NULL)
   {
        *p = 20;
   }
    return 0;
}

(八)字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}

还有一种使用方式如下: 

int main()
{
 const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?

 printf("%s\n", pstr);
 return 0;
}

注意:代码  const char* pstr = "hello bit.";特别容易误以为是把字符串 hello bit 放到字符指针 pstr 里了,但是/本质是把字符串 hello bit. 首字符的 地址放到了pstr中。

理解char型字符数组与char型指针关系

#include <stdio.h>

 
int main()
{
 char str1[] = "hello bit.";
 char str2[] = "hello bit.";
 const char *str3 = "hello bit.";
 const char *str4 = "hello bit.";
 
 if(str1 ==str2)
 printf("str1 and str2 are same\n");
 else

 printf("str1 and str2 are not same\n");
 
 if(str3 ==str4)
 printf("str3 and str4 are same\n");
 else

 printf("str3 and str4 are not same\n");
 
 return 0;
}

 运行结果:

理解 :

  

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针 指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会 开辟出不同的内存块。所以str1和str2不同,str3和str4相同。 

#include <stdio.h>
int main(int argc, char *argv[])
{
    char *p = "abc";
    p = "123";//yes
    *p = '1';//erro
    printf("%s\n",p);
    return 0;
}

为什么p可以赋值"123",而*p不能赋值'1'? 

字符串常量是不可改变的,但是指针变量可以改变指向的地址。在这个例子中,变量p是一个指向字符的指针,初始时指向字符串常量"abc"的首地址。然后通过p = "123"的赋值语句,将p的指向改变为字符串常量"123"的首地址。 而*p=‘1’的意思是改变字符串"abc"的一个值,而"abc"是一个字符串常量,所以会报错

#include <stdio.h>
int main(int argc, char *argv[])
{
    char str[10] = "abc";
    str = "123";//erro
    *str = '1';//erro
    str[0] = '1';//ok
    printf("%s\n",str);
    return 0;
} 

 为什么str不能赋值"123"?

在C语言中,字符串常量是以字符数组的形式存在的,该程序中声明了一个字符数组str,并将其初始化为字符串常量"abc"。然后你尝试将字符串常量"123"赋值给str,这是不允许的 。

 为什么 *str = '1'是错误的?

因为“abc”是常量,将首元素地址赋给str,数组str拷贝一份值,*str改变了常量“abc”的值,而常量不可以被修改,所以错误。

为什么str[0] = '1' 是正确的?

 因为“abc”是常量,将首元素地址赋给str,数组str拷贝一份值,数组是在栈里面,str[0] = '1',改变了数组里面的值,所以是可以的。

(九)二级指针

#include<stdio.h>
int main()
{
    int a = 10;
    int *pa = &a;//pa是指针变量,一级指针

    //ppa就是一个二级指针变量
    int **ppa = &pa;//pa也是个变量,&pa取出pa在内存中的起始地址

    return 0;
}

(十)指针数组

#include<stdio.h>
int main()
{
    int arr[10];//整形数组 - 存放整形的数组就是整型数组
    char ch[5];//字符数组 - 存放的是字符
    //指针数组 - 存放指针的数组
    int *parr[5];//整形指针的数组
    char *pch[5];//字符指针的数组
    return 0;
}

(十一) 数组指针 

数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉:

  • 整形指针: int * pint; 能够指向整形数据的指针。
  • 浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];

//p1, p2分别是什么?

解释:

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。

//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

 数组指针的使用

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
     int i = 0;
     for(i=0; i<row; i++)
     {
         for(j=0; j<col; j++)
         {
             printf("%d ", arr[i][j]);
         }
     printf("\n");
     }
}

void print_arr2(int (*arr)[5], int row, int col)
{
     int i = 0;
     for(i=0; i<row; i++)
     {
         for(j=0; j<col; j++)
         {
             printf("%d ", arr[i][j]);
         }
         printf("\n");
     }
}

int main()
{
     int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
     print_arr1(arr, 3, 5);
     //数组名arr,表示首元素的地址
     //但是二维数组的首元素是二维数组的第一行
     //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
     //可以数组指针来接收
     print_arr2(arr, 3, 5);
     return 0;
}

数组传参和指针传参 

一维数组传参 

1.指针传递:将数组的首地址作为参数传递给函数,函数中通过指针访问数组元素。

void func(int *arr, int size) {
    for (int i = ; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[]);
    func(arr, size);
    return ;
}

2.数组传递:直接将数组作为参数传递给函数。在函数声明时,可以指定数组的大小,也可以省略大小。

void func(int arr[], int size) {
    for (int i = ; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[]);
    func(arr, size);
    return ;
}

练习 

#include <stdio.h>
void test(int arr[])//ok? 正确
{}
void test(int arr[10])//ok?  正确
{}
void test(int *arr)//ok?  正确
{}
void test2(int *arr[20])//ok?  正确
{}
void test2(int **arr)//ok?  正确
{}
int main()
{
     int arr[10] = {0};
     int *arr2[20] = {0};
     test(arr);
     test2(arr2);
}
二维数组传参 
void test(int arr[3][5])//ok?  正确
{}
void test(int arr[][])//ok?  错误
{}
void test(int arr[][5])//ok?  正确
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int *arr)//ok?  错误
{}
void test(int* arr[5])//ok?  错误
{}
void test(int (*arr)[5])//ok?  正确
{}
void test(int **arr)//ok?  错误
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);
}
一级指针传参
#include <stdio.h>
void print(int *P,int sz)
{
    int i = 0;
    for(i=0;i<sz;i++)
    {
        printf("%d\n",*(p+1));
    }
}
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]);
    //一级指针p,传给函数
    print(p,sz);
    return 0;
}
二级指针传参 
#include <stdio.h>
void test(int**ptr)
{
    printf( "num = %d\n",**ptr);
}
int main()
{
    int a = 10;
    int*pa = &a;//pa是一级指针
    int **ppa = &pa;//ppa是二级指针
    test(ppa);//传二级指针
    test(&pa);//传一级指针变量的地址
    int *arr[10] = {0};
    test(arr);//传一级指针数组
    return 0;
}    

总结:二级指针传参可以传

1、传二级指针

2、一级指针变量地址

3、传一级指针数组。

(十二)函数指针

#include <stdio.h>
void test()
{
     printf("hehe\n");
}
int main()
{
     printf("%p\n", test);
     printf("%p\n", &test);
     return 0;
}

#include <stdio.h>
int add(int a,int b){
    return a+b;
}
int main()
{
  返回值 函数指针名字 类型
    int (*pf)(int,int) = &add;//函数指针
    int ret = (*pf)(3,5);//()优先级比*高,所以pf要加()
    int ret = add(3,5);
    int ret = pf(3,5);//因为函数名==&函数名,所以由add(3,5)推出pf(3,5)
    //上面三个代码都是一样的
    return 0;
}

注意:数组名(首元素地址) != &数组名。但是,函数名 == &函数名 

(十三)函数指针数组

#include <stdio.h>
// 加法函数
int add(int a, int b) {
    return a + b;
}

// 减法函数
int subtract(int a, int b) {
    return a - b;
}

// 乘法函数
int multiply(int a, int b) {
    return a * b;
}

int main() 
{
    int (*pf1)(int,int) = add;//加法的函数指针
    int (*pf2)(int,int) = subtract;//减法的函数指针
    int (*pf1)(int,int) = multiply;//乘法的函数指针
    //函数指针数组
    int (*pfArr[3])(int,int);
    return 0;
}

(十四)指向函数指针数组的指针(了解)

函数指针数组的定义 

int (*p)(int,int);//函数指针
int (* p[4])(int,int);//函数指针数组
int (* (*p)[4])(int,int);//指向函数指针数组的指针

(十五)回调函数

回调函数定义

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

 首先演示一下qsort函数的使用:

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
     return (*( int *)p1 - *(int *) p2);
}
int main()
{
     int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
     int i = 0;
 
     qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
     for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
     {
         printf( "%d ", arr[i]);
     }
     printf("\n");
     return 0;
}

 使用回调函数,模拟实现qsort(采用冒泡的方式)。

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
     return (*( int *)p1 - *(int *) p2);
}

void _swap(void *p1, void * p2, int size)
{
     int i = 0;
     for (i = 0; i< size; i++)
    {
         char tmp = *((char *)p1 + i);
         *(( char *)p1 + i) = *((char *) p2 + i);
         *(( char *)p2 + i) = tmp;
     }
}

void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
     int i = 0;
     int j = 0;
     for (i = 0; i< count - 1; i++)
     {
         for (j = 0; j<count-i-1; j++)
         {
             if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
             {
                 _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
             }
         }
     }
}
int main()
{
     int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
     //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
     int i = 0;
     bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
     for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
     {
         printf( "%d ", arr[i]);
     }
     printf("\n");
     return 0;
}

十三、结构体 

 结构体是C语言中重要的知识点,结构体使得C语言有能力描述复杂类型,相当于Java中的对象

(一)结构体使用 

include<stdio.h>
struct Stu{//定义学生结构体
    char name[20];//名字
    int age;      //年龄
    char sex[5];  //性别
    char id[15]; //学号
}s;//声明结构体时定义变量s
//s是全局变量
int main()
{
    //定义结构体变量s1
    struct Stu s1;
    //结构体的初始化
    struct Stu s2 = {"张三", 20, "男", "20180101"};
    //为结构成员访问操作符
    printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
    //->操作符
    struct Stu *ps = &s;
    printf("name = %s age = %d sex = %s id = %s\n", 
            ps->name, ps->age, ps->sex, ps->id);
    return 0;
}

(二)结构体内存对齐

结构体对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

                对齐数 = 编译器默认的对齐数 与 该成员大小的较小值

  •                         VS中默认的值为8
  •                         Linux中没有默认对齐数,对齐数就是成员自身的大小

    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

注意:一般情况下,较大的数据类型会被优先排列在前面,以确保对齐方式的正确性。比如,一个结构体中包含一个int类型和一个char类型的成员变量,那么int类型的成员变量会被排列在char类型的成员变量之前。 

举例1: 

举例2: 

举例3:

练习:

//练习1
struct S1
{
    char c1;
    int i;
    char c2;
};

printf("%d\n", sizeof(struct S1));//结果:12

//练习2
struct S2
{
    char c1;
    char c2;
    int i;
};

printf("%d\n", sizeof(struct S2));//结果:8

//练习3
struct S3
{
    double d;
    char c;
    int i;
};

printf("%d\n", sizeof(struct S3));//结果:16

//练习4-结构体嵌套问题
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};

printf("%d\n", sizeof(struct S4));//结果:32

(三)修改默认对齐数

我们可以用#pragma pack()改变我们的默认对齐数

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(2)//设置默认对齐数为2
struct S2
{
    char c1;
    int i;
    char c2;
};

#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

(四)结构体传参

struct S
{
    int data[1000];
    int num;
};

struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
    printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
    printf("%d\n", ps->num);
}

int main()
{
    print1(s);  //传结构体
    print2(&s); //传地址
    return 0;
}

上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。

结论: 

结构体传参的时候,要传结构体的地址。 

(五)位段 

什么是位段? 

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

3.位段后面的数字表示bit位。

比如: 

struct A
{
    int _a:2;//2bit
    int _b:5;//5bit
    int _c:10;//10bit
    int _d:30;//30bit
};

 位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。

 总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

十四、枚举 

(一)枚举的定义 

#include <stdio.h>
enum Color//颜色
{
    RED,
    GREEN,
    BLUE
};
int main(){
    printf("%d\n",RED);//0
    printf("%d\n",GREEN);//1
    printf("%d\n",BLUE);//2
    return 0;
}

默认从0开始,依次递增1 

#include <stdio.h>
enum Color//颜色
{
    RED=5,
    GREEN,
    BLUE
};
int main(){
    printf("%d\n",RED);//5
    printf("%d\n",GREEN);//6
    printf("%d\n",BLUE);//7
}

赋初始值后,从赋值的后面默认递增1

(二)枚举的使用 

enum Color//颜色
{
    RED=1,
    GREEN=2,
    BLUE=4
};
enum Color clr = 5;//不同的编译器可能会报错(整形赋值给枚举型)
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

 (三)枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?

枚举的优点:  

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 便于调试
  4. 使用方便,一次可以定义多个常量

 十五、联合体(共用体)

 (一)联合体的定义

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。  

//联合类型的声明
union Un
{
    char c;//1
    int i;//4
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));//4:最大成员的大小(int类型:4)

(二)联合的特点 

联合的成员是共用同一块内存空间(地址相同)的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

union Un
{
    int i;
    char c;
};
union Un un;
// 下面输出的结果是一样的吗?是一样的
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));

(三)联合体大小的计算

联合体大小就是最大成员的大小吗? 

union Un1
{
    char c[5];//5(看成5个char类型)
    int i;//4
};

union Un2
{
    short c[7];//7(看成7个short类型)
    int i;//4
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));//8:2x4=8
printf("%d\n", sizeof(union Un2));//16:4x4=16

十六、动态内存分配

 (一)malloc和free

void* malloc (size_t size);  

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。  

  1. 如果开辟成功,则返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  3. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  4. 如果参数 size 0malloc的行为是标准是未定义的,取决于编译器。

 C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);  

free函数用来释放动态开辟的内存。 

  1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  2. 如果参数 ptr NULL指针,则函数什么事都不做。
#include <stdio.h>
int main()
{
    //代码1
    int num = 0;
    scanf("%d", &num);
    int arr[num] = {0};
    //代码2
    int* ptr = NULL;
    ptr = (int*)malloc(num*sizeof(int));
    if(NULL != ptr)//判断ptr指针是否为空
    {
        int i = 0;
        for(i=0; i<num; i++)
        {
            *(ptr+i) = 0;
        }
    }
    free(ptr);//释放ptr所指向的动态内存给操作系统(地址还在)
    ptr = NULL;//将地址置空(不置空会非法访问已经还给操作系统的空间)
    return 0;
}

malloc值传递问题 

以下代码能输出”hello world”吗,如果不能请写出改正的方法,使代码能正确输出”hello world” 

void GetMemory(char *p)
{
    p = (char *)malloc(100);
}

void Test(void) 
{
    char *str = NULL;
    GetMemory(str);	
    strcpy(str, "hello world");
    printf(str);
}

 不能输出,因为GetMemory是值传递,为什么是值传递?因为最终是要改变str的内容,要修改内容必须&str的地址,而函数里直接传的是str的内容,所以是值传递。

修改如下: 

void GetMemory(char **p)
{
    *p = (char *)malloc(100);
}
void Test(void) 
{
    char *str = NULL;
    GetMemory(&str);	
    strcpy(str, "hello world");
    printf("%s",str);
    free(str);
}

类似的,下面代码是值传递还是地址传递?

void update(int *num){
    *num = 100;
}

int main()
{
    int num = 5;
    int *p = &num;
    update(p);
    return 0;
}

 是地址传递,因为最终要改变的是num的值,所以要&num的地址,而&num又赋值给了p,传入p给函数,就相当于传入了num的地址,所以是地址传递。

下面为链表部分代码 ,销毁函数里为什么用二级指针? 

typedef int data_t;
typedef struct node_t
{
    data_t data;
    struct node_t *next;
}linknode_t,*linklist_t;

//销毁链表
void destroy_linklist(linklist_t *head){
    clear_linklist(*head);
    free(*head);
    *head = NULL;
}

 要修改地址的值需要传地址的地址(二级指针),否则就是值传递

道理如下(举例说明):

#include <stdio.h>
//实现将a的地址改为b的地址方法
void fun(int **pa,int **pb){
    *pa = *pb;
}
int main()
{
    int a=5,b=10;
    int *pa=&a,*pb=&b;
    fun(&pa,&pb);
    printf("a=%d\n",*pa);//a=10
    return 0;
}

传一级指针错误图示: 

传二级指针正确图示: 

总结:要改值就要在方法里传该值的地址,要改地址就要在方法里传地址的地址(二级指针) 

 (二)calloc

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int *p = (int*)calloc(10, sizeof(int));
    if(NULL != p)
    {
    //使用空间
    }
    free(p);
    p = NULL;
    return 0;
}

 C语言还提供了一个函数叫 calloc calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);  

  1. 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  2. 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

 (三)realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

 函数原型如下:

void* realloc (void* ptr, size_t size); 

  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
  • realloc在调整内存空间的是存在两种情况:
  •                  情况1:原有空间之后有足够大的空间
  •                  情况2:原有空间之后没有足够大的空间  

情况 1
当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况 2
当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况, realloc 函数的使用就要注意一些

举个例子:

#include <stdio.h>
int main()
{
    int *ptr = (int*)malloc(100);
    if(ptr != NULL)
    {
     //业务处理
    }
    else
    {
        exit(EXIT_FAILURE);    
    }
     //扩展容量
     //代码1
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
 
     //代码2
    int*p = NULL;
    p = realloc(ptr, 1000);
    if(p != NULL)
    {
        ptr = p;
    }
    //业务处理
    free(ptr);
    return 0;
}

(四)常见的动态内存错误

1 NULL指针的解引用操作 

void test()
{
     int *p = (int *)malloc(INT_MAX/4);
     *p = 20;//如果p的值是NULL,就会有问题
     free(p);
}

 2 对动态开辟空间的越界访问

void test()
{
     int i = 0;
     int *p = (int *)malloc(10*sizeof(int));
     if(NULL == p)
     {
         exit(EXIT_FAILURE);
     }
     for(i=0; i<=10; i++)
     {
         *(p+i) = i;//当i是10的时候越界访问
     }
     free(p);
}

3 对非动态开辟内存使用free释放 

void test()
{
     int a = 10;
     int *p = &a;
     free(p);//ok?
}

4 使用free释放一块动态开辟内存的一部分

void test()
{
     int *p = (int *)malloc(100);
     p++;
     free(p);//p不再指向动态内存的起始位置
}

 5 对同一块动态内存多次释放

void test()
{
     int *p = (int *)malloc(100);
     free(p);
     free(p);//重复释放
}

 6 动态开辟内存忘记释放(内存泄漏)

void test()
{
     int *p = (int *)malloc(100);
     if(NULL != p)
     {
         *p = 20;
     }
}
int main()
{
     test();
     while(1);
}

总结:忘记释放不再使用的动态开辟的空间会造成内存泄漏。

切记:动态开辟的空间一定要释放,并且正确释放  

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值