Android-NDK-004-C基础

228 阅读51分钟

1 基本知识

image.png

image.png

  • 假定我們的计算机的字长8位 1字节 = 8bit (实际上int是4字节,16位) ,为了表示负数,将最高位解释为符号位
  • 示例1:-10的原码 补码 反码
-10的二进制表示
    原码  1000 1010  = -10
    反码  1111  0101
    补码  1111  0110
  • 示例2: 求0 1 -1 的补码
0   补码  0000 0000
1   补码  0000 0001 
-1  补码  1111 1111
  • 在计算机里面 数值 一律用补码来表示(存储)

2 数据类型

  • 字符型, 整型, 浮点型 image.png image.png

  • %输出介绍

%c 输出的是一个单字节字符

%f 输出的是一个单精度实行数

%d 输出的是一个十进制的整形变量,如果前面加了数字,例如:2%d意思是右对齐2格,左补空格。

%e 输出的是一个科学计数法形式的实型数

%u 表示对无符号整型数据的输出

%g 输出一个双精度实型数

%s 输出的是一个字符串

3 关键字

关键字描述
auto声明自动变量
double声明双精度变量或函数
typedef用以给数据类型取别名
register声明寄存器变量
short声明短整型变量或函数
char声明字符型变量或函数
const声明只读变量
static声明静态变量int声明整型变量或函数
struct声明结构体变量或函数
unsigned声明无符号类型变量或函数
volatile说明变量在程序执行中可被隐含地改变long声明长整型变量或函数
union声明共用数据类型
signed声明有符号类型变量或函数
void声明函数无返回值或无参数,声明无类型指针float声明浮点型变量或函数
enum声明枚举类型
extern声明变量是在其他文件正声明
if条件语句
else条件语句否定分支(与 if 连用)
switch用于开关语句
case开关语句分支
for一种循环语句do循环语句的循环体
while循环语句的循环条件
goto无条件跳转语句
continue结束当前循环,开始下一轮循环break跳出当前循环
default开关语句中的"其他"分支
sizeof计算数据类型长度
return子程序返回语句(可以带参数,也可不带参数)循环条件

4 变量

变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。

  • 变量的声明有两种情况: 1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建 立了存储空间。 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量 名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定 义的。 除非有extern关键字,否则都是变量的定义。
extern int i; //声明,不是定义
int i; //声明,也是定义

C 中的左值(Lvalues)和右值(Rvalues)

  • 两种类型的表达式:
  1. 左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达 式。左值可以出现在赋值号的左边或右边。
  2. 右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址 的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出 现在赋值号的右边,但不能出现在赋值号

5 常量

常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。

5.1 整数常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表 示十六进制,0 表示八进制,不带前缀则默认表示十进制。 整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数 (unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

  • 下面列举几个整数常量的实例:
212 /*合法的 */
215u /*合法的 */ 
0xFeeL /*合法的 */ 
078 /*非法的: 
8不是八进制的数字 */ 
032UU /*非法的:不能重复后缀 */ 
  • 以下是各种类型的整数常量的实例:
85 /*十进制 */ 
0213 /*八进制 */ 
0x4b /*十六进制 */ 
30 /*整数 */ 
30u /*无符号整数 */ 
30l /*长整数 */ 
30ul /*无符号长整数 */ 

5.2 浮点常量

浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式 或者指数形式来表示浮点常量。 当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使 用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数 是用 e 或 E 引入的。

  • 下面列举几个浮点常量的实例:
3.14159 /*合法的 */ 
314159E-5L /*合法的 */ 
510E /*非法的:不完整的指数 */ 
210f /*非法的:没有小数或指数 */ 
.e55 /*非法的:缺少整数或分数 */ 

5.3 字符常量

字符常量是括在单引号中,例如,'x' 可以存储在 char 类型的简单变量中。

212 /* 合法的 */
215u /* 合法的 */
0xFeeL /* 合法的 */
078 /* 非法的:8 不是八进制的数字 */
032UU /* 非法的:不能重复后缀 */
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* 整数 */
30u /* 无符号整数 */
30l /* 长整数 */
30ul /* 无符号长整数 */
3.14159 /* 合法的 */
314159E-5L /* 合法的 */
510E /* 非法的:不完整的指数 */
210f /* 非法的:没有小数或指数 */
.e55 /* 非法的:缺少整数或分数 */

转义序列含义

\       \ 字符
'       ' 字符
"       " 字符
\?      ? 字符
\a       警报铃声
\b       退格键
\f       换页符
\n       换行符
\r       回车
\t       水平制表符
\v       垂直制表符
\ooo     一到三位的八进制数
\xhh     一个或多个数字的十六进制数

字符常量可以是一个普通的字符(例如 'x')、一个转义序列(例如 '\t'),或一 个通用的字符(例如 '\u02C0')。 在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含 义,被用来表示如换行符(\n)或制表符(\t)等。下表列出了一些这样的转义 序列码: 下面的实例显示了一些转义序列字符:

5.4 实例

#include <stdio.h> 
int main() { 
        printf("Hello\tWorld\n\n"); 
        return 0; 
}

当上面的代码被编译和执行时,它会产生下列结果:

Hello	World

5.5 字符串常量

字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的 字符:普通的字符、转义序列和通用的字符。 您可以使用空格做分隔符,把一个很长的字符串常量进行分行。 下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。

Hello World
"hello, dear"
"hello, \
dear"
"hello, " "d" "ear"

5.6 定义常量

  • 两种简单的定义常量的方式: 使用 #define 预处理器。 使用 const 关键字。

6 存储类

auto 是局部变量的默认存储类,限定变量只能在函数内部使用; register 代表了寄存器变量,不在内存中使用; static 是全局变量的默认存储类,表示变量在程序生命周期内可见; extern 表示全局变量,即对程序内所有文件可见,类似于Java中的public关键字;

6.1 全局变量、局部变量、静态全局变量、静态局部变量的区别

6.1.1 从作用域看:

1、全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。 2、静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。 3、局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。 4、静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

6.1.2 从分配内存空间看:

1、全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间 2、全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

1)静态变量会被放在程序的静态数据存储区(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。 
2)变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static这个说明符在不同的地方所起的作用是不同的。应予以注意。

6.1.3 Tips:

A. 若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度; B. 若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度; C. 设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题,因为他们都放在静态数据存储区,全局可见; D. 如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为:带"内部存储器"功能的的函数) E. 函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针

7 作用域规则

任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C语言中有三个地方可以声明变量: 1.在函数或块内部的局部变量 2.在所有函数外部的全局变量 3.在形式参数的函数参数定义中

8 头文件

头文件是扩展名为 .h的文件,包含了 C函数声明和宏定义,被多个源文件中引用共享

  • 只引用一次头文件 如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
#ifndef HEADER_FILE #define HEADER_FILE 
the entire header file file 
#endif 

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

  • 有条件引用 有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:
#if SYSTEM_1 
# include "system_1.h" 
#elif SYSTEM_2 
# include "system_2.h" 
#elif SYSTEM_3 
... 
#endif 

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头 文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include的直接参数,您只需要使用宏名称代替即可:

#define
SYSTEM_H "system_1.h" 
... 
#include SYSTEM_H 

SYSTEM_H会扩展,预处理器会查找 system_1.h,就像 #include最初编写的那样。SYSTEM_H可通过 -D选项被您的 Makele定义。

9 预处理器

C预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤(预处理阶段)。简言之,C预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C预处理器( C Preprocessor)简写为 CPP。 所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令  描述 
#define      定义宏 
#include     包含一个源代码文件 
#undef       取消已定义的宏 
#ifdef       如果宏已经定义,则返回真 
#ifndef      如果宏没有定义,则返回真 
#if          如果给定条件为真,则编译下面代码 #else #if的替代方案 
#elif        如果前面的 #if给定条件不为真,当前条件为真,则编译下面代码 
#endif       结束一个 
#if……
#else        条件编译块 
#error       当遇到标准错误时,输出错误消息 
#pragma      使用标准化方法,向编译器发布特殊的命令到编译器中

9.1 预处理器运算符

C预处理器提供了下列的运算符来帮助您创建宏:

  • 延续运算符(\) 一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:
#define message_for(a, b) \ 
printf(#a " and " #b ": We love you!\n") 
  • 字符串常量化运算符(#) 在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:
#include <stdio.h> 
#define message_for(a, b) \ printf(#a " and " #b ": We love you!\n") 
int main(void) 
  { 
      message_for(Carole, Debra); return 0; 
  } 

当上面的代码被编译和执行时,它会产生下列结果: Carole and Debra: We love you!

  • 标记粘贴运算符(##) 宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:
#include <stdio.h> 
#define tokenpaster(n) printf ("token" #n " = %d", token##n) 
int main(void) { 
    int token34 = 40; 
    tokenpaster(34); return 0; 
} 

当上面的代码被编译和执行时,它会产生下列结果:

token34 = 40 

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

printf ("token34 = %d", token34); 

这个实例演示了 token##n 会连接到 token34中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。

  • defind运算符 预处理器defind运算符是用在常量表达式中的,用来确定一个标识符是否已经使用#dene定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined()运算符的用法:
#include <stdio.h> 
#if !defined (MESSAGE) #define MESSAGE "You wish!" #endif 
int main(void) 
{ 
  printf("Here is the message: %s\n", MESSAGE); return 0; 
} 

当上面的代码被编译和执行时,它会产生下列结果:

Here is the message: You wish! 

9.2 参数化的宏

CPP一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

int square(int x) { 
    return x * x; 
} 

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x)) 

在使用带有参数的宏之前,必须使用 #de指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

#include <stdio.h> 
#define MAX(x,y) ((x) > (y) ? (x) : (y)) 
int main(void) 
{ 
printf("Max between 20 and 10 is %d\n", MAX(10, 20)); 
return 0; 
} 

当上面的代码被编译和执行时,它会产生下列结果:

Max between 20 and 10 is 20 

10 typedef

C语言提供了 typedef关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:

typedef unsigned char BYTE; 

在这个类型定义之后,标识符 BYTE可作为类型 unsigned char的缩写,例如:

BYTE b1, b2; 
  • typedef vs #dee

#define是 C指令,用于为各种数据类型定义别名,与 typedef类似,但是它们有以下几点不同:

  1. typedef仅限于为类型定义符号名称,#define不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1为 ONE。
  2. typede是由编译器执行解释的,#de语句是由预编译器进行处理的。
  • 运算符优先级: image.png

  • 记忆: image.png

11 指针

指针是一个变量,其值为地址。声明指针或者不再使用后都要将其地址置为0 (NULL) 在计算机中0地址不能被访问 野指针 未初始化的指针 悬空指针 指针最初指向的内存已经被释放了的一种指针

  • 定义
int *a; 正规
int* a;
int * a;
//因为 其他写法看起来有歧义
int* a,b;
  • 使用
//声明一个整型变量
int i = 10;
//将i的地址使用取地址符给p指针
int *p = &i;
//输出 0xffff 16进制地址
printf("%#x\n", &i);
printf("%#x\n", &p);
  • 指针多少个字节? 指向地址,存放的是地址, 地址在 32位系统中指针占用4字节 64系统中指针占用8字节
//32位:
sizeof(p) == 4;
//64位:
sizeof(p) == 8;

11.1 解引用 *

解析并返回内存地址中保存的值

int i = 10;
int *p = &i;
//解引用
//p指向一个内存地址,使用*解出这个地址的值 即为 10
int pv = *p;
//修改地址的值,则i值也变成100
//为解引用的结果赋值也就是为指针所指的内存赋值
*p = 100;

11.2 指针运算

//对指针 进行算数运算
int main02() {

    //指针的算术运算
    //1, 对于变量,不同的类型,占用的空间大小是不一样
    //2, 对于不同类型的指针变量,它们占用的空间都是相同的
    //为什么指针要加类型,不能只用通用类型的:  为了指针算术运算

    int i = 100;
    int *prt_i = &i;
    printf("prt_i=%p\n", prt_i);
    printf("prt_i+1=%p\n", prt_i + 1);//int类型4个字节,所以该指针加1,地址值+4
    printf("prt_i+2=%p\n", prt_i + 2);

    short c = 100;
    short *prt_c = &c;
    printf("prt_c=%p\n", prt_c);
    printf("prt_c+1=%p\n", prt_c + 1);//short类型2个字节,所以该指针加1,地址值+2
    printf("prt_c+2=%p\n", prt_c + 2);



    //总结:
    // 1 指针占4位,步进长度与其所指类型有关
    //2 只能进行加法和减法:+,-, ++,--,+=,-=

    return 0;
}
  • 运行结果
prt_i=000000000061FDDC
prt_i+1=000000000061FDE0
prt_i+2=000000000061FDE4
prt_c=000000000061FDDA
prt_c+1=000000000061FDDC
prt_c+2=000000000061FDDE

指针指向地址,指针运算实际上就是移动指针指向的地址位置,移动的位数取决于指针类型(int就是32位) image.png

11.3 指针和一维数组

在c语言中,指针和数组名都表示地址 1、数组是一块内存连续的数据 2、指针是一个指向内存空间的变量

11.3.1 数组表达式

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

  • 规则1:表达式中的数组名被编译器当做一个指向该数组第一个元素的指针,是数值的起始地址,数组名是个常量指针。数组名;注:下面几种情况例外: (1) 数组名作为sizeof的操作数 (2) 使用&取数组的地址
  • 规则2:下标总是与指针的偏移量相同;
  • 规则3:在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。 规则1和规则2结合在一起理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。如a[i]总是被编译器解析为 *(a+i)的形式。

11.3.2 一维数组

  • &p,p,a,&a &p:表示取存储指针变量p的内存单元的地址(指针指向的地址); sizeof(&p)=4; p:表示取指针变量p存储的地址(指针的地址); sizeof(p)=4; a:表示取数组第一个元素的地址; sizeof(a)=3*4=12; &a:表示取整个数组的首地址; sizeof(&a)=4

示例:

int main03() {

    //指针与一维数值  
    //数组名就是数值的起始地址,也是第一个元素的地址,数组名就是一个常量指针
    int array[] = {1, 2, 3, 4, 5};
    int *ptr_int = array;
    // ptr_int = ptr_int +1;
    printf("ptr_int=%p\n", ptr_int);//指针的地址
    printf("&array=%p\n", &array);//数组的地址
    printf("&array[0]=%p\n", &array[0]);//数组第一个元素的地址
    printf("=====================================\n");

    //通过指针下标操作数组数据
    ptr_int[2] = 100;
    //通过移动指针修改数据
    *(ptr_int + 3) = 200;


    printf("ptr_int[3]=%d\n", ptr_int[3]);//ptr_int[3]值
    printf("=====================================\n");

    //array+3,&array[3], ptr_int+3,&ptr_int[3] 代表是同一个地址
    printf("array+3=%p\n", array + 3);//1
    printf("&array[3]=%p\n", &array[3]);//2
    printf("ptr_int+3=%p\n", ptr_int + 3);//3
    printf("&ptr_int[3]=%p\n", &ptr_int[3]);//4
    printf("=====================================\n");
    printf("*(array+3)=%d\n", *(array + 3));//1
    printf("array[3]=%d\n", array[3]);//2
    printf("*&array[3]=%d\n", *&array[3]);//2
    printf("*(ptr_int+3)=%d\n", *(ptr_int + 3));//3
    printf("*&(ptr_int[3])=%d\n", *&(ptr_int[3]));//4
    printf("=====================================\n");
    return 0;
}
  • 执行结果
ptr_int=000000000061FDD0
&array=000000000061FDD0
&array[0]=000000000061FDD0
=====================================
ptr_int[3]=200
=====================================
array+3=000000000061FDDC
&array[3]=000000000061FDDC
ptr_int+3=000000000061FDDC
&ptr_int[3]=000000000061FDDC
=====================================
*(array+3)=200
array[3]=200
*&array[3]=200
*(ptr_int+3)=200
*&(ptr_int[3])=200
=====================================

Process finished with exit code 0

###11.3.3 数组指针和指针数组

  • 指针数组: 首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
  • 数组指针: 首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
int main04() {

    //数组指针与指针数组

    // int (*array_ptr)[5] 数组指针(指针)
    // ()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度

    // int *ptr_array[5]  指针数组(数组)
    // []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,
    // 这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。
    // 但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。

    int array[] = {1, 2, 3, 4, 5};//数组
    int (*array_ptr)[5] =  &array;//array_ptr为数组指针(指针)
    int *ptr_array = array;//ptr_array为指针数组(数组)

    printf("array=%p\n", array);//首地址,第一个元素的地址
    printf("&array=%p\n", &array);//数组地址
    printf("array_ptr=%p\n", array_ptr);//数组指针(指针)地址
    printf("ptr_array=%p\n", ptr_array);//指针数组(数组)地址

    printf("=====================================\n");

    printf("array+1=%p\n", array + 1);//array代表数组的的地址,也是第一个元素的地址,一个元素占用四个字节,偏移下一个地址,其实就是array[1]的地址
    printf("&array[1]=%p\n", &array[1]);//array[1]的地址
    printf("ptr_array+1=%p\n", ptr_array + 1);//arry_ptr指针数组的地址+1

    printf("=====================================\n");

    printf("&array+1=%p\n", &array + 1);//偏移的array[5]整个数组 (包含5个int类型数据的数组, 20字节)
    printf("&array+1=%p\n", &array[4] + 1);//array[4]地址+1
    printf("array_ptr+1=%p\n", array_ptr + 1);//数组指针(指针)+1,相当于要跨过n个整型数据的长度(包含5个int类型数据的数组, 20字节)

    return 0;
}
  • 执行结果
array=000000000061FDC0
&array=000000000061FDC0
array_ptr=000000000061FDC0
ptr_array=000000000061FDC0
=====================================
array+1=000000000061FDC4
&array[1]=000000000061FDC4
ptr_array+1=000000000061FDC4
=====================================
&array+1=000000000061FDD4
&array+1=000000000061FDD4
array_ptr+1=000000000061FDD4

Process finished with exit code 0

11.3.4 指针运算测试

/**
 * 指针运算测试
 * @return 
 */
int main05() {

    int array[] = {1, 2, 3, 4, 5};
//    int *ptr_array = &array;//错误的 把它当成了一个int类型的指针

//    int (*ptr_array)[4] = &array;//这里会编译报错,(*)[4] = &array-> (*)[5] 类型不同
    int (*ptr_array)[5] = &array;//数组指针

    int *p = NULL;

    int i = 0;
    for (i = 0; i < 5; i++) {
        printf("array[%d]=%d, *(array +%d)=%d\n", i, array[i], i, *(array + i));
        printf("&array[%d]=%p, array+%d=%p\n", i, &array[i], i, array + i);
    }

    printf("=====================================\n");
    
    //将数组直接赋值给*p,结果是指针数组,以指针的方式,移动指针的方式,访问数组
    for (p = array; p < array + 5; p++) {
        printf("p=%p, *p=%d\n", p, *p);
    }

    return 0;
}
  • 面试题:
#include <stdio.h>

int main(void)
{
    int a[5]={1,2,3,4,5};
    int *ptr=(int *)(&a+1);
    printf("%d,%d",*(a+1),*(ptr-1));
    return 0;
}

结果: 2,5 ###11.3.5 sizeof()运算 在C中, 在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针“。——《C和指针》 在以下两中场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度,而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。所以&a后返回的指针便是指向数组的指针,跟a(一个指向a[0]的指针)在指针的类型上是有区别的。——《C和指针》 "+1",偏移量的问题,一个类型为T的指针的移动,是以sizeof(T)为移动单位即array+1:在数组首元素的首地址的基础上,偏移一个sizeof(array[0])单位,此处的类型T就是数组中的一个int型的首元素即&array+1:在数组的首地址的基础上,偏移一个sizeof(array)单位。此处的类型T就是数组中的一个含有5个int型元素的数组

/**
 * sizeof()运算
 * @return
 */
int main06() {
    int array[5] = {0};
    printf("        array = %p\n", array);
    printf("       &array = %p\n", &array);
    printf("    array + 1 = %p\n", array + 1);
    printf("&array[0] + 1 = %p\n", &array[0] + 1);
    printf("   &array + 1 = %p\n", &array + 1);
    printf("\n");
    printf(" sizeof(int*) = %d\n", sizeof(int*));
    printf(" sizeof(&array[0]) = %d\n", sizeof(&array[0]));
    printf(" sizeof(0xffffcc04) = %d\n", sizeof(0xffffcc04));
    printf(" sizeof(array) = %d\n", sizeof(array));//数组类型的长度
    printf("sizeof(&array) = %d\n", sizeof(&array));//计算的是指针的长度
    printf("\n");
    return 0;

}

11.4 指针和二维数组

11.4.1 指向指针的指针

一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指 针的地址,第二个指针指向包含实际值的位置。

int main03() {//作业 find可以 find2不行 swap

    //1 指针的指针示例
    int i = 100;
    int *pInt = &i;
    int **ppInt = &pInt;//指针的指针
    printf("i         =%d\n",i);
    printf("*pInt     =%d\n",*pInt);//pInt取值为100
    printf("*ppInt    =%d\n",*ppInt);//ppInt取值为pInt(指针的地址)
    printf("**ppInt   =%d\n",**ppInt);//ppInt取值为pInt(指针的地址)后再取值为100
}
  • 执行结果
i         =100
*pInt     =100
*ppInt    =6421988
**ppInt   =100
  • 多级指针的意义 可用于查找.
void main{
  //2 二级指针查找字符,返回元素位置和该元素开始的string值
    char str[] = "huan ying lai xuexi";
    char *ret = NULL;
    int i1 = find1(str, 'y', &ret);//这里传入指针ret的地址作为参数,代表二级指针指向该地址
    printf("index=%d\n", i1);//输出查找的位置
    printf("*ret=%c\n", *ret);//输出查找之后指针ret当前指向的内容
    printf("ret =%s\n", ret);//输出查找之后指针ret(常量指针)指向的内容
    printf("=====================================\n");
}
int find1(const char *src, char ch, char **ret) {
    int count = 0;
    char *index = (char *) src;

    while (*index) {

        //*index取出值=y时
        if (*index == ch) {
            //将该地址赋值为指针ret
            *ret = index;//这里是将index的地址给了*ret,传入的是**ret,相当于改变了外部指针的指向地址,类似于swap()交换数据
            //循环结束,返回1
            return count;
        }
        index++;
        count++;
    }
    return 0;
}
  • 执行结果
index=5
*ret=y
ret =ying lai xuexi
=====================================

11.4.2 二维数组

  • 示例
int main06() {
    int array[5] = {0};
    printf("        array = %p\n", array);
    printf("       &array = %p\n", &array);
    printf("    array + 1 = %p\n", array + 1);
    printf("&array[0] + 1 = %p\n", &array[0] + 1);
    printf("   &array + 1 = %p\n", &array + 1);
    printf("=====================================\n");
    printf(" sizeof(int*)       = %d\n", sizeof(int*));
    printf(" sizeof(&array[0])  = %d\n", sizeof(&array[0]));
    printf(" sizeof(0xffffcc04) = %d\n", sizeof(0xffffcc04));
    printf(" sizeof(array)      = %d\n", sizeof(array));//数组类型的长度
    printf(" sizeof(&array)     = %d\n", sizeof(&array));//计算的是指针的长度

//    在C中, 在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针“。——《C和指针》
//    在以下两中场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度,而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。所以&a后返回的指针便是指向数组的指针,跟a(一个指向a[0]的指针)在指针的类型上是有区别的。——《C和指针》
    //"+1",偏移量的问题,一个类型为T的指针的移动,是以sizeof(T)为移动单位
    //即array+1:在数组首元素的首地址的基础上,偏移一个sizeof(array[0])单位,此处的类型T就是数组中的一个int型的首元素
    //即&array+1:在数组的首地址的基础上,偏移一个sizeof(array)单位。此处的类型T就是数组中的一个含有5个int型元素的数组
    printf("=====================================\n");
    return 0;

}

  • 执行结果
        array = 000000000061FDD0
       &array = 000000000061FDD0
    array + 1 = 000000000061FDD4
&array[0] + 1 = 000000000061FDD4
   &array + 1 = 000000000061FDE4
=====================================
 sizeof(int*)       = 8
 sizeof(&array[0])  = 8
 sizeof(0xffffcc04) = 4
 sizeof(array)      = 20
 sizeof(&array)     = 8

12 函数

12.1 动态内存分配函数

#define _CRT_ALLOCATION_DEFINED
  void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
  void __cdecl free(void *_Memory);
  void *__cdecl malloc(size_t _Size);
  void *__cdecl realloc(void *_Memory,size_t _NewSize);

12.1.1 为什么要使用动态内存分配???

 以一个数组为例:在定义一个数组时给定了其内存空间,只要给定足够大的空间,就可以放入你所需的数据元素。但当内存空间小于所需放入的元素个数时,我们就需要给这个内存空间去增容,以此来满足你的需求。
  上述定义数组时设置足够大的内存空间看似简单,但却有以下缺点:
        1.如果内存空间很大,数据却很少就会使内存空间浪费。
        2.如果内存空间不足就会出现溢出的现象。

12.1.2.malloc

 1.   函数原型

//有文件
#include<stdlib.h>
 
//源文件
void* calloc(size_t num_elements,size_t element_size);

malloc的参数就是需要所分配的字节数。

2. malloc在C语言中不是关键字而是C函数库中提供的函数。如果需要进行内存分配时在调用malloc时就是在内存池中提取一块内存空间(在堆上申请一块空间),但是这块空间是连续的空间。,并向该程序返回一个这块内存的指针。

注意:在申请完空间后需要自己去初始化这块空间,如果懒得动手那就可以用realloc(下面会介绍)

3.malloc进行内存分配的特点:

**    (1.)所申请的空间是连续的**

**    (2.)在有的编译器下申请的空间略大于你请求申请的内存空间**

**    (3.)malloc所返回的内存起始位置将始终能够满足对边界对齐**

4.malloc是如何知道你所申请的内存空间是什么类型?

**     这是因为在使用malloc进行内存分配时会返回void*类型的指针,但是这个指针可以转化为任意类型的指针。(但是有些老式的编译器需要你去强转你所需要的类型)**

#include<Windows.h>
#include<stdio.h>
#include<malloc.h>
 
int main()
{
	int* p = NULL;
	printf("%x\n", p);
	p = (int*)malloc(sizeof(int)* 25);
	if (NULL == p)
	{
 
		printf("申请失败!!!");
		return 0;
 
	}
	printf("%x\n", p);
	system("pause");
	return 0;
}

注意:由上边代码可以看到当用malloc动态分配空间后一定要进行对申请的空间进行判断是否申请失败,这点很重要。

12.1.2 calloc

**    函数原型:**

//有文件
#include<stdlib.h>
 
//源文件
void* calloc(size_t num_elements,size_t element_size);

calloc和calloc的动态分配内存是有相同之处,他们的主要区别在于:

(1.)在函数原型中calloc多了一个参数(他的参数是由所需元素的个数和每个元素的字节数组成)

(2.)最大区别:calloc在调用完后返回指向内存的指针之前把他初始化为0

#include<Windows.h>
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
 
int main()
{
	int* p = NULL;
	int* p1 = NULL;
 
	p = (int*)malloc(sizeof(int)* 5);
	if (NULL == p)
	{
 		printf("申请失败!!!");
		return 0;
 	}
  
	printf("malloc:\n");
 
	for (int i = 0; i < 5; i++)
	{
		printf(" %d-->", *p);
		*p++;
 	}
	printf("\n");
 
	p1 = (int*)calloc(5, sizeof(int));
	if (NULL == p1)
	{
 		printf("申请失败!!!");
		return 0;
 	}
 
	printf("calloc:\n");
	for (int j = 0; j < 5; j++)
	{
		printf(" %d-->",*p1);
		*p1++;
 	}
	
	system("pause");
	return 0;
}

image

注意:在调用完calloc后也要进行判断是否申请成功

12.1.3 realloc

**  函数原型:**

//头文件
#include<stdlib.h>
 
//原型
void realloc(void *ptr, size_t new_size);

ptr是指向原来地址的指针。

这个函数用于修改一个原先已经分配内存块的大小。

使用:

1.可以使一块内存扩大或缩小(原来的内存块可以扩大缩小)

**  (1.)如果是扩大一个内存块,则将原来的内存块保留在他的后边新增一块内存块(但是新增的内存块并未初始化)**

**  (2.)如果是缩小一块内存块,则将该内存块的后半部分直接拿掉,剩余部分内存块及其内容保留。**

2.原来的内存块无法扩大缩小

**   如果是着这种情况,realloc会重新开辟一个新的内存空间,并把原来的内存空间的内容拷贝到新的内存空间里。**

注意:再调用完realloc后就不能使用指向就内存的指针,而是用返回的新的指针。

image

值得一提的是:如果realloc中的第一个参数如果为空则和malloc一样。

#include<Windows.h>
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
 
int main()
{
	const int size = 2000;
	int *p = (int *)malloc(20 * sizeof(int));
	int *p1 = (int *)realloc(p, size*sizeof(int));
	
	printf("%x\n", p);
	printf("%x\n", p1);
	system("pause");
	return 0;
}

image

12.1.4 free(释放空间)

函数原型:

free(void* pointer);

free的参数必须要不时空,要不然就必须是malloc,calloc,realloc中返回值。

注意:在使用free释放空间后,指针变量要置为NULL,不做该处理的话,该指针为悬空指针。

* p= str;
free(str);
str=NULL;

12.1.5 .内存空间

image

以上的操作都是在堆上进行

12.2 指针作为函数的参数

  • 传值调用 把参数的值复制给函数的形式参数。修改形参不会影响实参
  • 引用调用 形参为指向实参地址的指针,可以通过指针修改实参。
void change1(int *i) {
    *i = 10;
}
void change2(int *i) {
    *i = 10;
}
int i = 1;
change1(i);
printf("%d\n",i); //i == 1
change2(&i);
printf("%d\n",i); //i == 10

12.3 数组作为函数参数

int main01() {

    int array[5] = {1, 2, 3, 4, 5};//C语言 它不会去检查数组下标越界的情况
    test(array, 5);
    printf("array[0]-test=%d\n", array[0]);
    printf("=====================================\n");

    //array作为指针参数传递,会被编译成指针
    test1(array);
    printf("array[0]-test1=%d\n", array[0]);
}
/**
 * 数组作为参数
 * @param array
 * @param size
 */
void test(int array[], int size) {//作业//
//    printf("sizeof(array) = %d\n", sizeof(array));//这里的用sizeof计算array占用字节长度是错误的

    int *index = array;
    int length = 0;
    while (*index) {

        size_t i = sizeof(*index);
        printf("sizeof(*index) = %d\n", i);//
        length += i;
        index++;
    }

    printf("=====================================\n");
    printf("sizeof(array) = %d\n", length);//
    printf("=====================================\n");

    //改变数组的值
    array[0] = 100;
}

/**
 * 指针作为参数
 * @param array
 */
void test1(int *array) {

    //改变指针指向的0位置的值
    *(array + 0) = 100;
}
  • 运行结果
sizeof(*index) = 4
sizeof(*index) = 4
sizeof(*index) = 4
sizeof(*index) = 4
sizeof(*index) = 4
=====================================
sizeof(array) = 20
=====================================
array[0]-test=100
=====================================
array[0]-test1=100

12.4 函数指针

12.4.1 函数指针: 指向函数的指针变量

int main02() {
    
    void (*p)(int);   //定义一个函数指针(是一个指针)
    // void * p(int) //根据优先级,p(int)是个整体,p(int)代表一个函数,void* 代表是函数的返回值,(是一个函数,该函数的返回值类型是void类型的指针)

    //将函数地址赋值给函数指针
    p = &test2;
    //通过函数调用
    test2(22);
    printf("=====================================\n");
    //通过函数指针调用
    p(100);
    printf("=====================================\n");
    
    return 0;
}
void test2(int n) {
    printf("test2 %d ... \n", n);
}
  • 执行结果
test2 22 ...
=====================================
test2 100 ...
=====================================

12.4.2 函数回调

/**
 *
 * @param a
 * @param b
 * @param callback  函数指针为参数(该函数指针有两个参数)
 * @return
 */
int test3(int a, int b, int (*callback)(int, int)) {
    return (callback(a, b));
}

int call1(int i, int j) {
    return i + j;
}

int call2(int i, int j) {
    return i - j;
}

void main03() {

    printf("xxx =%d\n", test3(1, 2, call1));
    printf("xxx =%d\n", test3(1, 2, call2));

    // 指针最重要的 就是 用指针的时候 你脑子里面一定要明确的知道你这个指针它指向哪儿
    // int *p
    // int *p[n]
    // int (*p)[n]
    // int *p()
    // int (*p)()
    // int ** p
}

  • 执行结果
xxx =3
xxx =-1

13 从两个例子分析 C语言的声明

在C语言中,声明的形式和使用的形式相似,这种用法可能是 C语言的独创,K& R也承认"C语言声明的语法有时候会带来严重的问题"。C语言的声明存在的最大问题是你无法以一种人们所习惯的自然方式从左到右阅读一个声明。

  • 下面看一个例子:
char * const *(*next)(); 

如果在第一眼就能看出这个声明要表达的意思,那么证明你的C语言功底已经到了一定的程度。《C专家编程》一书中给出的识别步骤为:

  1. 从变量名next开始,并注意到它直接被括号括住;
  2. 所以先把括号里的东西作为一个整体,得出"next”是一个指向....的指针;
  3. 然后考虑括号外面的东西,在星号前缀和括号后缀之间做一个选择;
  4. 根据C语言声明的优先级规则(后面会给出),优先级较高的是右边的函数括号,所以得出"next"是一个函数指针,指向一个返回...的函数;
  5. 然后,,处理前缀"*",得出指针所指的内容;
  6. 最后,把"char * const *"解释为指向字符串的常量指针。

把上述结果加以概括,这个声明表示"next是一个指针,它指向一个函数,这个函数返回另一个指针,该指针指向一个类型为char的常量指针"。这个问题便迎刃而解了.

  • 下面再看一个例子:
char *(* c[10])(int **p); 

首先,从变量名c开始,然后处理后缀"[]",表明 c是一个数组,接着处理前缀"*",表示c是一个指针数组。然后处理后面的括号,表明数组c中的指针类型是指向一个函数的指针,并且这个函数的参数有且仅有一个:为指向指针的指针,该函数的返回值为一个指向字符串的指针。归纳在一起,为: "c是一个数组[0...9],它的元素类型是函数指针,其所指向的函数返回值是一个指向字符串的指针,并且把一个指向指针的指针作为唯一的参数"。 以下是《C专家编程》一书中提到的C语言声明的优先级规则,摘自第64页。 image.png

  • 例子
  1. 数组指针:
int (*p)[5]  //这里的 p是一个指针,指向一个具有 5个元素的数组指针
  1. 指针数组:
int *p[5]    //这里的 p是一个数组,数组中的元素类型是 int* 

14 常量指针和指针常量的区别

  • 常量指针: 表示const修饰的为所申明的类型。例如:
//注意 char const *p与const char *p效果相同。 
void consttest(const char *p) 
{ 
printf("p[1]=%c\n",p[1]); 
p=1;//正确 
*(p+1)='a';//错误 
} 

因为const修饰的是char,所以就是说:p所指向的内存地址所对应的值,是 const,因此不可修改。但指针所指向的内存地址是可以修改的,因为其并不是 const类型。

  • 指针常量: 表示const修饰的指针。 例如:
void testconst(char *const p) 
{ 
char *tmp="13213"; p=1;;//错误 p=tmp;;//错误 p[1]='a';//正确 *(p+1)='a';//正确 
} 

因为const修饰的是指针p,也就是说:指针所指向的内存地址是const,不可修改。但p所指向内存地址所对应的值是可以修改的,因为其并不是 const类型。 指向常量的指针常量: const同时修饰类型和指针。只读 例如:

void consttestconst(const char *const p) //引用 { 
p=1;//错误 p[1]='a';//错误 } 

因为const同时修饰这类型和指针,也就是说:指针所指向的内存地址不可修改同时内存地址所对应的值也不可修改。

总结: 主要看的就是const所处的位置(在谁的左边就是修饰谁)。 1)const在数据类型的左边:表示const修饰的为所申明的类型。常量指针 2)const在指针的左边:表示const修饰的为指针。指针常量 3)前*后均有:表示const同时修改类型和指针。指向常量的指针常量

const的优点在C/C++中关键字const用来定义一个只读的变量或者对象,有如下优点: (1)便于类型检查,如函数的函数fun(const int a)a的值不允许变,这样便于保护实参 (2)功能类似与宏定义,方便参数的修改和调整。如const int max = 100; (3)节省空间,如果再定义a= max,b=max。。。就不用在为max分配空间了,而用宏定义的话就一直进行宏替换并为变量分配空间 (4)为函数重载提供参考 常量指针和指针常量的区别下面通过一个例子来解析常量指针和指针常量,我们先总结一下常量指针和指针常量的区别首先一定要明白哪种定义方式是常量指针,哪种是指针常量,这里可以记住三句话加深记忆: (指针)和const(常量)谁在前先读谁;*象征着地址,const象征着内容;谁在前面谁就不允许改变。

好吧,让我们来看这个例子:

int a =3; 
int b = 1; 
int c = 2; 
int const *p1 = &b;//const在前,定义为常量指针
int *const p2 = &c;//*在前,定义为指针常量 
  1. 常量指针p1: 指向的地址可以变,但内容不可以重新赋值,内容的改变只能通过修改地址指向后变换。p1=&a是正确的,但*p1 = a是错误的。
  2. 指针常量p2: 指向的地址不可以重新赋值,但内容可以改变,必须初始化,地址跟随一生。p2= &a是错误的,而*p2 = a是正确的。

关于常量指针,指针常量的知识点补充 1.概念:常量指针是指 –指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。指针常量是指 –指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。注:也有人将这两个名称的定义与含义反过来认为: “指针常量:顾名思义它的中心词是“常量”这是重点,指针就是一个修饰的作用。所以这里的指针还是一个变量,它的内容存放的是常量的地址。常量指针:关键字是指针,它是不能被改变的,因为指针总是指向地址的,所以它的意思是它指向的地址是不能被改变的”。但我个人认为后者不合理,所以使用前者。 2.使用方法:使用时写法上的区别:常量指针:const在之前指针常量: const在之后。当然我们也可以定义常量指针常量,那就需要加上两个const,一前一后!以上只是从定义上给出两者的本质上的不同,在具体使用上,还有很多变化,但万变不离其宗,我们可以根据它的原理分析出各种复杂用法的实质。

  • 使用技巧 使用指针常量可以增加代码的可靠性和执行效率。如:
Int a; 
Int * const p =&a; 

增加可靠性:不用担心p被修改或释放导致非预期结果;增加执行效率:不用在子函数中对p做为空检查可以提高效率。

15 字符串

15.1 字符串的表示

  • 示例
int main1() {

    //C语言没有字符串的概念,实际上是字符数组来表示
//    " aaa string"
//    "3234sadsk \ 折行符
//     sddd"

    //定义数组
    int a[10];//int数组
    char b[10];//字符数组

    //C语言字符串用字符数组表示
    char str1[10] = {'h', 'e', 'l', 'l', 'o', '\0'};//以`\0`结束的特殊的字符数组代表是字符串,,如果没有则是普通的字符数组
    char str2[10] = "hello";//'\0'可以省略的,同样时代表字符串,在栈区,没有占全的地方,填充0
    char *str3 = "hello";//str3指针在栈区,"hello"在常量区,可以读不可修改
    //效果是不是一样的
    printf("str1=%s\n", str1);
    printf("str2=%s\n", str2);
    printf("str3=%s\n", str3);//这里str3指向常量区,这里取值是不需要用*str3

    return 0;
}
  • 输出结果
str1=hello
str2=hello
str3=hello

15.2 接收输入字符串

  • 示例
int main2() {

    //方式一:
    printf("输入一个字符串\n");
    char str1[10];
    scanf("%s", str1);//1.输入空格会结束接收,导致接收不完整  2. 溢出问题会导致崩溃
    printf("str1=%s\n", str1);
    printf("=====================================\n");


    //方式二
    printf("再输入一个字符串\n");
    char str2[11];
//    gets(str2);//同样溢出问题会导致崩溃
    fgets(str2, 11, stdin);//
    printf("str2=%s\n", str2);
    return 0;
} 
  • 输出结果
输入一个字符串
hello word
str1=hello
=====================================
再输入一个字符串
hello word
str2=hello word

15.3 给字符数组赋值

  • 示例
/**
 * 计算字符数组的长度
 * @param str
 * @return
 */
int mystrlen(char *str) {
    int i = 0;
    while (*(str + (++i)));//'\0' ASCII 0 非零为true
    return i;

int main3() {

    char str[10] = "abc";
//    //循环
    int i;
    for ( i = 0; i < 10; i++) {
        str[i] = 'i';
    }
    // str = i i i i i i i i i i
    printf("str=%s\n", str);//str结尾本身会有个\0折行符
    printf("sizeof(str)=%d\n", sizeof(str));//不能用sizeof()函数获取字符数组的长度,结果是错误的
    printf("strlen(str)=%d\n", strlen(str));//用strlen()函数才能获取真实的长度,'\0'算进去了
    printf("mystrlen(str)=%d\n", mystrlen(str));//调用自实现计算字符数组长度函数
    printf("=====================================\n");

    //strcpy()函数给str字符数组赋值
    strcpy(str,"dekrje");//通过该函数赋值,结尾不会添加'\0'
    printf("str=%s\n", str);//

    printf("sizeof(str)=%d\n", sizeof(str));//不能用sizeof()函数获取字符数组的长度,结果是错误的
    printf("strlen(str)=%d\n", strlen(str));//用strlen()函数才能获取真实的长度
    printf("mystrlen(str)=%d\n", mystrlen(str));//调用自实现计算字符数组长度函数

    return 0;
}

  • 执行结果
str=iiiiiiiiii

sizeof(str)=10
strlen(str)=11
mystrlen(str)=11
=====================================
str=dekrje
sizeof(str)=10
strlen(str)=6
mystrlen(str)=6

15.4 字符串拼接

  • 示例
int main4() {

    printf("字符串拼接:\n");

    char s1[] = "abc";//字符数组形式定义的字符串才能实现拼接
    char s2[] = "123";//变量名和字符串都在栈内存,可读可写
    // char *s11= "abc";//这种形式的字符串无法实现拼接
    // char *s21 = "123";//变量名在栈内存,字符串在常量区,可读,不可修改
     mystrcat(s1, s2);
    printf("s1 = %s\n", s1);
    printf("s2 = %s\n", s2);
    printf("=====================================\n");


    //字符数组形式字符串接收输入值
    char s3[10];
    scanf("%s",s3);
    printf("s3=%s\n",s3);

    //指针形式字符串接收输入值
    char *s4 = (char*)calloc(10,sizeof(char));//calloc函数,是在堆内存开辟空间
    scanf("%s",s4);
    printf("s4=%s\n",s4);
    printf("=====================================\n");

    //字符串copy
    char *s5 = "he";
    strcpy(s1, s5);//strcpy(s1, s5),将s5的值,赋值给s1
    printf("s1 = %s\n", s1);
    printf("s5 = %s\n", s5);

    char s6[10] = "abc";
    strncpy(s6, "123456", 3);//strncpy( , , )第三个参数代表长度
    printf("s6=%s\n", s6);
    printf("=====================================\n");

    //strcmp()函数用来比较字符串是否相等
    printf("%d\n", strcmp("a", "a"));//相等返回0
    printf("%d\n", strcmp("a", "b"));//不等返回-1

    return 0;
}
  • 运行结果
字符串拼接:
s1 = abc123
s2 = 123
=====================================
123456
s3=123456
123456
s4=123456
=====================================
s1 = he
s5 = he
s6=123
=====================================
0
-1

16 结构体与共用体

16.1 结构体定义和赋值

* 示例
/**
 * struct定义结构体
 */
struct Books {
    char title[100];
    char author[10];
    int book_id;
} book = {"c语言基础", "zero", 1};//这里直接初始化结构体

int main1() {
    printf("=====================================\n");

    //结构体赋值,并访问结构体
    struct Books book1;
    strcpy(book1.title, "c programming");
    strcpy(book1.author, "av");
    book1.book_id = 2;
    printf("sizeof(book1) =%d\n", sizeof(book1));
    printf("book1.title   = %s\n", book1.title);
    printf("book1.author  = %s\n", book1.author);
    printf("book1.book_id = %d\n", book1.book_id);
    printf("=====================================\n");


    //定义结构体类型的指针
    struct Books *p;
    p = &book1;//该指针指向结构体,可用指针访问结构体
    
    //方式1
    printf("p->title = %s\n", p->title);
    printf("p->author = %s\n", p->author);
    printf("p->book_id = %d\n", p->book_id);
    printf("=====================================\n");
    //方式2
    printf("p->title = %s\n", (*p).title);
    printf("p->author = %s\n", (*p).author);
    printf("p->book_id = %d\n", (*p).book_id);
    printf("=====================================\n");


    return 0;
}
  • 输出结果
=====================================
sizeof(book1) =116
book1.title   = c programming
book1.author  = av
book1.book_id = 2
=====================================
p->title = c programming
p->author = av
p->book_id = 2
=====================================
p->title = c programming
p->author = av
p->book_id = 2
=====================================

Process finished with exit code 0

16.2 共用体

  • 示例
/**
 * union定义共用体
 */
union Data {
    int i;
    float f;
    char str[20];
};

17 C中文件读写

17.1 打开文件

FILE *fopen( const char * filename, const char * mode );
  • r : 打开一个已有的文本文件,允许读取文件。
  • w: 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
  • a: 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
  • r+: 打开一个文本文件,允许读写文件。
  • w+: 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
  • a+: 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。
  • b: 以二进制方式打开文件
  • t: 以文本方式打开文件(默认方式下以文本方式打开文件)
/**
 * 打开/创建文件 fopen( , )
 * @return
 */
int main1() {
    printf("文件打开\n");

    //fopen()函数,打开文件
    FILE *p = fopen("./test/b.txt", "w");//使用相对路径需要设置当前项目的工作空间
//    FILE *p = fopen("D:\\Project\\C\\Practice_C\\file\\test\\a.txt", "w");

    if (p) {
        //文件存在或创建成功
        printf("读取=success\n");
        //fclose()函数,关闭文件流
        fclose(p);
        p = NULL;
    } else {
        printf("读取=fail\n");
    }


    return 0;
}

17.2 字符读写

字符读写主要使用两个函数fputc和fgetc,两个函数的原型是:

  • int fputc(int ch,FILE *fp);//若写入成功则返回写入的字符,否则返回-1
  • int fgetc(FILE *fp); //若读取成功则返回读取的字符,否则返回-1
/**
 * 字符读取  int fgetc(FILE *fp);//若读取成功则返回读取的字符,否则返回-1
 * @return
 */
int main2() {

    printf("读取字符: int fgetc(FILE *fp);//若读取成功则返回读取的字符,否则返回-1\n");

    FILE *p = fopen("./file/test/a.txt", "r");
    if (p) {
        printf("读取=success\n");

        while (1) {
            char c = fgetc(p);//一次读取一个字符
            if (c == EOF)//EOF=-1,
                break;//读取失败跳出循环

            printf("%c", c);//
        }

        fclose(p);//关闭
        p = NULL;
    } else{
        printf("读取=fail\n");

    }


    return 0;
}
/**
 * 写入字符
 *
 */
void main3() {
    printf("写入字符: int fputc(int ch,FILE *fp);//若写入成功则返回写入的字符,否则返回-1\n");

    //打开文件操作
    FILE *p = fopen("./file/test/b.txt", "w");//以w打开会清空之前的文件

    if (p) { ;
        printf("读取=success\n");

        fputs("我是写入的内容", p);//
        fclose(p);
        p = NULL;
    } else{
        printf("读取=fail\n");

    }
}

17.3 字符串读写

  • int fputs(const char *s,FILE *fp); //将字符串写入文件,若写入成功则返回一个非负值,否则返回-1;
  • char *fgets(char *s,int n,FILE *fp); //从文件中读取不超过n-1个字符到字符数组中(若文件中字符少于n-1个,则只读取文件中存在的字符),系统在字符数组末尾自动添加一个'\0',返回字符数组的首地址 //对于fgets函数,在读取过程中,若读取到字符'\n',则读取过程提前结束

17.4 块读写

  • unsigned int fread(void *buffer,unsigned int size,unsigned int n,FILE *fp); //从文件读取一组数据存放在首地址为buffer的内存空间中,size为一个数据块的大小,n为要读取的数据块的个数,若读取成功,则返回读取的数据的数据块的个数,否则返回0.
  • unsigned int fwrite(const void *buffer,unsigned int size,unsigned int n,FILE *fp); //向文件中写入数据,写入成功返回写入数据块的个数,否则返回0.

17.5 格式化读写

  1. 格式化读写和其他几种读写有很大的不同。格式化读写是以我们人所能识别的格式将数据写入文件,即若以格式化方式写入一个整型数值65,则其实是写入的两个字符'6'和'5',即占2字节,而不是4字节,但是若以块写方式写入,则其占4字节。即在使用格式化读写时系统自动进行了一些转换。
  2. fprintf和fscanf函数一般成对出现,若数据是用fprintf进行写入的,则最好使用fscanf进行读取。
  3. 在使用fprintf函数写入时,若文件是以文本方式打开,如果参数format中包含了'\n',则最后文件中会被写入换行符;而若文件以二进制方式打开,则文件中不会被写入换行符
  • int fscanf(FILE *fp,const char *format[,argument]....); //用于从文件格式化读取数据,若读取成功,则返回读取的数据个数,否则返 回-1
  • int fprintf(FILE *fp,const char *format[,argument]....); //用于向文件格式化写入数据,若写入成功,则返回写入的字符个数,否则返 回-1
  • int sscanf(const char *str, const char *format, ...) //从字符串读取格式化输入。
  • int sprintf(char *str, const char *format, ...) //发送格式化输出到 str 所指向的字符串

17.6 其他几个常见操作

17.6.1 移动位置指针的函数

  • void rewind(FILE *fp); // 将位置指针移动到文件首
  • int fseek(FILE *fp,long int offset,int origin); //将位置指针移动到距离origin的offset字节数的位置
  1. origin的值有三个: SEEK_SET(0) 文件首, SEEK_CUR(1) 当前位置,SEEK_END(2) 文件尾 若文件是以a追加方式打开,则当进行写操作时,这两个函数是不起作用的,无论将位置指针移动哪个位置,始终将添加的数据追加到文件末尾
  2. 只有用 r+ 模式打开文件才能插入内容,w 或 w+ 模式都会清空掉原来文件的内容再来写,a 或 a+ 模式即总会在文件最尾添加内容,哪怕用fseek() 移动了文件指针位置

17.6.2 stat函数

//通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
#include<sys/stat.h>
int stat(const char *filename,struct stat *_stat)
//返回的结构体
struct stat {
    mode_t st_mode; //文件对应的模式,文件,目录等
    ino_t st_ino; //inode节点号
    dev_t st_dev; //设备号码
    dev_t st_rdev; //特殊设备号码
    nlink_t st_nlink; //文件的连接数
    uid_t st_uid; //文件所有者
    gid_t st_gid; //文件所有者对应的组
    off_t st_size; //普通文件,对应的文件字节数
    time_t st_atime; //文件最后被访问的时间
    time_t st_mtime; //文件内容最后被修改的时间
    time_t st_ctime; //文件状态改变时间
    blksize_t st_blksize; //文件内容对应的块大小
    blkcnt_t st_blocks; //伟建内容对应的块数量
};

17.6.3 ftell函数

  • long int ftell(FILE *fp); //计算当前位置指针距文件首的字节数,若出错,则返回-1L。 //利用ftell函数可以计算出文件的大小。

17.6.4 feof函数

  • int feof(FILE *fp); //检测当前位置指针是否到达文件末尾,若到达文件末尾,则返回一个非零 值,否则返回0。

17.6.5 ferror函数

  • int ferror(FILE *fp); //检测文件操作过程中是否出错,若出错,则返回一个非零值,否则返回0

17.6.6 remove函数

  • int remove(const char *filename); //删除文件,若删除成功,则返回0,否则返回非零值

17.6.7 rename函数

  • int rename(const char *oldname,const char *newname); //将文件重命名,重命名成功则返回0,否则返回非零值。

17.6.8 freopen函数

  • FILE* freopen(const char *filename,const char *mode,FILE *stream); //实现重定向输入输出。此函数在测试数据时用得比较多。

17.6.9 fclose函数

  • int fclose(FILE *stream); //关闭一个流,若成功,则返回0,否则返回-1.注意每次对文件操作完之后需 关闭流,否则可能会造成数据丢失。

17.6.10 flush函数

  1. 由于flush是实时的将缓冲区的内容写入磁盘,所以不要大量去使用,
  2. 只要特别敏感的数据,需要及时写入防止丢失,比如密码
  • int fflush(FILE *_file); //可以将缓冲区中任何未写入的数据写入文件中,成功返回0,失败返回EOF