C语言学习笔记2

发布时间 2023-07-16 11:35:40作者: 张彧520

数组

所谓数组,就是一个集合,里面存放了相同类型的数据元素
特点:数组中的每个数据元素都是相同的数据类型,数组是由连续的内存位置组成的。

一维数组

一维数组定义方式3种:
1数据类型 数组名 [数组长度];
创建一个数组,[]里给一个常量表达式,不能是变量。
2数据类型 数组名 [数组长度] = {值1,值2...};
3数据类型 数组名 [] = {值1,值2...};
数组的下标是从0开始索引的,可以利用循环输出数组中的元素。
[]下标引用操作符
数组在内存地址中是连续存放的。

eg:
int arr[10] = {1,2,3};	//不完全初始化,剩下的元素默认初始化值为0
char arr1[] = "abcdef";
sizeof(arr1)	//7 内存空间有一个\0。7个元素-char 7*1=7
strlen(arr1);	//6 /0空字符不计算在内
char arr2[] = {'a','b','c'};
sizeof(arr2)	//3 3个元素-char 3*1=3
strlen(arr2);	//随机值,遇不到\0不知道在那哪里结束。返回的是无符号数值。

数组名是首元素的地址(这两个除外)

1 sizeof(数组名) 表示:数组名是整个数组,计算的是整个数组的大小,单位字节
2 &数组名,数组名代表整个数组,取出的是整个数组的地址

用途

1可以统计整个数组在内存中的长度
数组长度=sizeof(arr)/sizeof(arr[0]);
2可以获取数组在内存中的首地址
数组首地址为:printf(%p\n,arr);
数组中第一个元素地址为:printf(%p\n,&arr[0]);
eg:
#include <stdio.h>
int main(){
    int arr[5]={1,2,3,4,5};
    printf("%p\n",arr);
    printf("%p\n",&arr[0]);
    return 0;
}

二维数组

定义方式4种:
1数据类型 数组名 [行数][列数];
2数据类型 数组名 [行数][列数]={{数据1,数据2},{数据3,数据4}};
3数据类型 数组名 [行数][列数]={数据1,数据2,数据3,数据4};
4数据类型 数组名 [][列数]={数据1,数据2,数据3,数据4};

同一维数组一样,二维数组也是连续存储的。

用途

1可以查看占用内存空间
二维数组占用内存空间:sizeof(arr)
二维数组第一行占用空间:sizeof(arr[0])
二维数组第一个元素占用空间:sizeof(arr[0][0])
二维数组行数:sizeof(arr)/sizeof(arr[0])
二维数组列数:sizeof(arr[0])/sizeof(arr[0][0])
2可以查看二维数组的首地址
二维数组首地址为:printf(%p\n,arr);


C语言中PRINTF和SCANF函数使用的格式化输出和输入标志主要有:
%d - 输出十进制整数
%i - 输出十进制整数
%o - 输出八进制整数
%x, %X - 输出十六进制整数
%u - 输出十进制无符号整数
%c - 输出字符
%s - 输出字符串
%f - 输出浮点数
%e, %E - 输出科学计数法表示的浮点数
%g, %G - 自动选择合适的浮点数格式输出
%p - 输出指针地址
%% - 输出百分号字符
此外还有宽度控制、精度控制等修饰标志,例如%-20d表示左对齐输出最小宽度为20的十进制整数。
除了这些标准的格式化输出标志,一些编译器还支持额外的格式化标志,如%a输出十六进制浮点数等。但不同编译器实现可能有所不同。

函数

作用:将一段经常使用的代码封装起来,减少重复代码。一个大的程序,一般分为若干个程序块,每个模块实现特定的功能。

函数定义

返回值类型 函数名(参数列表)
{
	函数体语句;
	return表达式;
}


返回值类型:一个函数可以返回一个值,在函数定义中
函数名:给函数起个名字
参数列表:使用该函数时,传入的数据
函数体语句:花括号内的代码,函数内需要执行的语句
return表达式:和返回值类型挂钩,函数执行完后,返回相应的数据

实参:真实传给函数的参数。实参可以是:常量,变量,表达式,函数等,无论实参是何种类型的量,在进行函数调用时,他们都必须有确定的值,以便把这些值传送给形参。

形参:形式参数是指函数名后括号中的变量,因为形式参数只有函数被调用的过程中才实例化分配内存单元,所以叫形式参数,形式参数当函数调用完成之后就自动销毁了,因此形式参数只在内存中有效。

函数的调用:
传值调用:函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。
传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,这种传参方式可以让函数和函数外边的变量建立起真正地联系,也就是函数内部可以直接操作函数外部的变量。

链式访问:把一个函数的返回值作为另外一个函数的参数。

函数可以嵌套调用,但不能嵌套定义。

函数调用:

使用定义好的函数,语法:函数名(参数)

#include <stdio.h>
int add(int num1,int num2){
    //定义中num1,num2称为形式参数,简称形参
    int sum=num1+num2;
    return sum;
}
int main(){
    //调用add函数
    int sum=add(1,2); //调用时1,2称为实际参数,简称实参
    printf("%d\n",sum);
    return 0;
}

当我们做值传递时,函数的形参发生改变,并不会影响实参。

函数的常见样式

1无参无返
2有参无返
3无参有返
4有参有返

老版编译器要先声明后调用,声明可以有多次,定义只有一次。

函数分文件编写

让代码结构更加清晰,函数分文件编写一般有4个步骤:
1创建后缀名为.h的头文件
2创建后缀名为.cpp的源文件(c语言是.c)
3在头文件中写函数的声明
4在源文件中写函数的定义

用的话文件前面加一下:#include <xxx.h>

指针

指针的作用:可以通过指针间接访问内存
内存编号是从0开始记录的,一般用16进制数字表示
可以利用指针变量保存地址

指针变量定义语法:数据类型 * 变量名;

使用指针:
可以通过解引用的方式来找到指针指向的内存
指针前加 * 代表解引用,找到指针指向的内存中的数据
可以读取和修改

指针-指针为个数。

在32位操作系统下,指针时占4个字节空间大小,不管是什么数据类型
在6位操作系统下,指针时占8个字节空间大小,不管是什么数据类型

空指针:指针变量指向内存中编号为0的空间
用途:初始化指针变量,空指针指向的内存是不可以访问的
0~255之间的内存编号是系统占用的,因此不可以访问

const修饰指针有三种情况:

1const修饰指针---常量指针
const int * p=&a;
特点:指针的指向可以修改,但是指针指向的值不可以改

2const修饰常量---指针常量
int * const p=&a;
特点:指针的指向不可以改,指针指向的值可以改

3const既修饰指针又修饰常量
const int * const p=&a;
特点:指针的指向和指针指向的值都不可以改

指针和数组
作用:利用指针访问数组中元素

指针和函数
作用:利用指针作函数参数,可以修改实参的值
#include <stdio.h>
int main(){
    int a=10;   //定义一个变量
    int * p=NULL;	//指针变量p指向内存地址编号为0的空间
    int * p;    //指针定义语法:数据类型 * 变量名
    p=&a;   //指针指向变量a的地址
    printf("%d\n",&a);  //打印a的地址
    printf("%d\n",p);   //打印指针p的地址
    *p=20;              //修改指针p对应地址的值也就是a的值
    printf("%d\n",a);   //打印a的值
    printf("%d\n",*p);  //打印指针p的值
    return 0;
}

指针和数组

#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr;  //定义一个指向arr数组的指针p
    printf("%d\n",arr[0]);  //打印数组第一个元素
    printf("%d\n",*p);      //打印指针p
    for(int i=0;i<10;i++){  //指针的遍历
        printf("%d",*p);
        p++;	//指针向后偏移(int)4个字节
    }
    return 0;
}

指针和函数

#include <stdio.h>
//值传递
int swap1(int a,int b){
        int temp=a;
        a=b;
        b=temp;
         printf("%d,%d\n",a,b);
}
    //地址传递
int swap2(int *p1,int *p2){
        int temp=*p1;
        *p1=*p2;
        *p2=temp;
        printf("%d,%d\n",*p1,*p2);
}
int main(){
    int a=10;
    int b=20;
    swap1(a,b);     //值传递,不能修改main函数实参
    swap2(&a,&b);   //地址传递,可以修改main函数实参
    printf("%d,%d\n",a,b);
    return 0;
}
    

习题:判断100-200之间的素数

#include <stdio.h>
#include <string.h>
#include <math.h>
int is_prime(int a) {
    
        int j = 0;
        for (int j = 2; j < sqrt(a); j++) {
            if (a % j == 0) {
                return 0;
            }
        }       
            return 1; 
    }

int main()
{
    int i = 0;
    for (i = 101; i <201;i++){
        if (is_prime(i) == 1) {
            printf("%d\n", i);
        }
    }
    return 0;
}

二分算法

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

int binary_rearch(int arr[], int k, int sz) {
	int left = 0;
	int right = sz-1;
	while (left<=right)
	{
        int mid = (left + right) / 2;
		if (arr[mid] < k) {
			left = mid + 1;
		}
		else if (arr[mid] > k) {
			right = mid - 1;
		}
		else {
			return mid;
		}
	}
	return -1;
}
int main() {
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_rearch(arr, k, sz);
	if (ret == -1) {
		printf("找不到\n");
	}
	else {
		printf("找到了,下标是:%d\n", ret);
	}
	return 0;
}

函数递归

什么是递归?
程序调用自身的编程技巧称为递归(recursion)。递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小。

递归的两个必要条件:
1存在限制条件 ,当满足这个限制条件的时候,递归便不再继续。
2每次递归调用之后越来越接近这个限制条件。

栈溢出(死循环) Stack overflow

利用递归简单打印1 2 3 4

#include <stdio.h>
#include <string.h>
void print(int n){
	if (n > 9) {
		print(n / 10);	//递归调用
	}
	printf("%d ", n % 10);
}
int main() {
	int num = 0;
	scanf("%d", &num);
	print(num);	//函数调用
	return 0;
}

自定义求字符串长度,创建零时变量

#include <stdio.h>
#include <string.h>
int my_strlen(char * str) {
	int count = 0;
	while(*str != '\0') {	//解引用
		count++;
		str++;
}
	return count;

}
int main() {
	char arr[] = "bit";
	//int len = strlen(arr);	求字符串长度
	//printf("%d\n", len);
	int len = my_strlen(arr);//arr是数组,数组传参,传过去的不是整个数组,而是第一个元素的地址
	printf("len= %d\n", len);
	return 0;
}

自定义求字符串长度,不创建零时变量

#include <stdio.h>
#include <string.h>
int my_strlen(char * str) {
		if(*str != '\0') {
			return 1 + my_strlen(str + 1);	//递归调用
		}
		else {
			return 0;
		}
}
int main() {
	char arr[] = "bit";
	//int len = strlen(arr);	求字符串长度
	//printf("%d\n", len);
	int len = my_strlen(arr);//arr是数组,数组传参,传过去的不是整个数组,而是第一个元素的地址
	printf("len= %d\n", len);
	return 0;
}

指针

指针是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为指针。意思是通过它能找到以它为地址的内存单元。

指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)

指针是用来存放地址的,地址是唯一标示一块地址空间的。

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

指针类型决定了指针进行解引用操作的时候,能够访问空间的大小。
int*p	*p能够访问4个字节
char*p	*p能够访问1个字节
double*p	*p能够访问8个字节

指针的类型决定了指针的步长。

指针数组是数组,是存放指针的数组。

野指针

概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)

野指针成因:
	1指针未初始化
	2指针越界访问
	3指针指向的空间释放
	
如何规避野指针:
	1指针初始化
	2小心指针越界
	3指针指向空间释放即使置NULL
	4指针使用之前检查有效性

例题

#include <stdio.h>
int main() {
	int a = 0x11223344;
	char* p = (char*)&a;
	*p = 0;
	printf("%x\n", a);	//%x用于输出十六进制格式
	return 0;
}

答应结果:11223300

C语言中十六进制对应的数字和字母如下:
0 - 0 
1 - 1
2 - 2
3 - 3
4 - 4
5 - 5
6 - 6 
7 - 7
8 - 8
9 - 9
A - 10
B - 11  
C - 12
D - 13
E - 14
F - 15
十六进制到十进制的转换方法:
1. 每个十六进制位代表0-15的一个值
2. 每位上的数字/字母需要乘以16的余数幂
3. 将每个位的结果相加即得十进制数
例如:
十六进制数:0x2B
转换步骤: 
1. 2 x 16^1 = 32
2. B x 16^0 = 11
3. 32 + 11 = 43
所以0x2B对应的十进制数是43。
类似的,可以用相同的方法将一个十进制数转换成十六进制数。


将十进制整数转换成十六进制的方法:
1. 不断对十进制整数进行除以16的运算,直到商为0
2. 将每一步的余数(0-15)记录下来
3. 将记录的余数逆序排列即为对应的十六进制数
例如,将十进制数123转换成十六进制:
1. 123 / 16 = 7......11   余数为11(B)
2. 7 / 16 = 0......7      余数为7 
3. 0 / 16 = 0            商为0,计算结束
将余数7和11逆序排列,即为十六进制数:
123的十六进制数是:7B
所以十进制数123对应的十六进制数是0x7B。
另外一种简单方法是利用编程语言提供的转换函数,例如C语言中可以使用sprintf():
int dec = 123; 
char hex[10];
sprintf(hex, "%x", dec); // hex = "7b"
这可以避免手动计算的麻烦。

例题
//例题
#include <stdio.h>
int i;
int main() {
	i--;
	if (i > sizeof(i)) {	//sizeof()计算变量/类型所占内存空间大小>=0 无符号数
		printf(">\n");		//-1和无符号数比较,符号位不看负数,-1的无符号数看类型计算
	}						
	else {
		printf("<\n");
	}
	return 0;
}
//在C语言中, -1的无符号数表示方式由两个部分组成:
//1. 无符号数(unsigned)关键字
//2. 整数类型(如unsigned int)
//对于不同的整数类型, 表示 - 1的无符号数的值会不同。
//例如 :
//1. unsigned int : -1的无符号数是4294967295(32位)
//2. unsigned short : -1的无符号数是65535(16位)
//3. unsigned char : -1的无符号数是255(8位)
//这是因为无符号数使用补码形式表示, 当给一个有符号整数的二进制表示取反后, 就得到了相应的无符号数。
//具体来说, -1的二进制表示全为1, 取反后就是全0, 该无符号数的值就是该整数类型能表示的最大值。
//所以对于不同位数的无符号整数类型, 表示 - 1的值都不相同, 但统一的规律是它们对应的最大值。

调试

调试(Debugging/Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

调试基本步骤:
发现程序错误的存在
以隔离,消除等方式对错误进行定位
确定错误产生的原因
提出纠正错误的解决办法
对程序错误予以改正,重新测试

Debug 通常称为调试版本:它包含调试信息,并且不做任何优化,便于程序员调试程序

Release 成为发布版本:它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用。

调试过程:
选择Debug调试
调试常用快捷键:
F5	启动调试,经常用来直接调到下一个断点处。
Ctrl+F5	开始执行不调试,如果你想要程序直接运行起来而不调试就可以直接使用。
F10	逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11	逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(最常用)。
F9	创建断点和取消断点,可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。

优秀的代码:
代码运行正常,bug很少,效率高,可读性高,可维护性高,注释清晰,文档齐全
常见的coding技巧:
使用assert,尽量使用const,养成良好的代码风格,添加必要的注释,避免编码的陷阱。

编程常见的错误:
编译型错误:
	直接看错误信息(双击),解决问题。
链接型错误
	看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
运行时错误
	借助调试,逐步定位问题。

自定义字符串拷贝,用到assert。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src) {
	char* ret = dest;
	assert(dest != NULL);//断言,传违规报错
	assert(dest != NULL);//断言
	//把src指向的字符串拷贝到dest指向的空间,包含'\0'字符
	while (*dest++ = *src++ ) {
		;
	}
	return ret;
}
int main() {
	char arr1[] = "#######";
	char arr2[] = "bit";
	printf("%s\n", my_strcpy(arr1, arr2));
	return 0;
}