重拾c语言基础-记录笔记
2018-10-31 本文已影响10人
未见哥哥
自从出来工作之后就没在用过 c 了,为了更加深入的学习一些底层的东西,不得已重新来过一遍基础的知识点。
C的基本数据类型
signed&unsigned
- int是4个字节,32位
- int能表示2的32次方个数字
- 把最高位抽出来,用于表示正负,只剩下31位表示数值
- 此时int能表示2的31次方
- 如果最高位的值是0,那么int就是正数,范围就是0~2的31次方-1
- 如果最高位的值是1,那么int就是负数,范围就是-(2的31次方)~-1
- 那么int的总范围,就是-2的31次方~2的31次方-1
- signed:有符号,表示最高位用于表示正负
- unsigned:无符号,表示最高位用于表示数值
- 无符号int型只能表示正数,并且取值范围是0~2的32次方-1
原码、反码、补码
- 原码:把数值转换成二进制数值
- 反码:除最高位,其他位依次取反
- 补码:在补码的基础上,除最高位,其他位依次取反,然后+1
- 正数的原码、反码、补码相同
基本数据类型的长度
JAVA
- byte:1
- short:2
- int:4
- long:8
- float:4
- double:8
- boolean:1
- char:2
C
- short:2
- int:4
- long:32位操作系统是4个字节,64位操作系统是8字节
- float:4
- double:8
- char:1
示例代码
#include <stdio.h>
int main(int argc, const char * argv[]) {
//基本数据类型的长度
//sizeof返回的 unsign long 无符号的long类型
printf("%lu\n",sizeof(int));//4
printf("%lu\n", sizeof(short));//2
printf("%lu\n", sizeof(long));//在32位操作系统中long的长度4个字节,64位操作系统中long的长度为8个字节
printf("%lu\n", sizeof(float));//4
printf("%lu\n", sizeof(double));//8
printf("%lu\n", sizeof(char));//1
printf("%lu\n", sizeof(long long));//8
//以下两种声明的方式,我觉得是没有必要的,因为要表示2个长度的数据类型,直接使用 short 就好的,要表8位长度的数据使用 long 就行了。
printf("%lu\n",sizeof(short int));//2
printf("%lu\n",sizeof(long int));//8
//定义一个无符号的变量a,最大值42亿
unsigned int a = 4294967295;
//最高位表示符号位,只能表示无符号的一半大小,默认定义的整形类型就是有符号位的
signed int b = 2100000000;
//如果使用无符号来表示-1的话,它表示的是无符号的最大值4294967295。
unsigned int c = -1;
printf("%u\n",a);
printf("%d\n",b);
printf("%u\n",c);
//char只能存放一个字符,并且使用''表示
char ch = 'b';
printf("%c",ch);
return 0;
}
指针
内存地址
- 内存中每一个字节都有一个内存地址,这样才能保证不管数据存在内存的哪一个位置,都可以通过内存地址找到。
- 访问内存的数据,要通过内存地址。
指针变量
- 指针变量就是保存内存地址的变量。
- 指针变量通常简称为指针。
- int* p;定义一个int型的指针变量,用于保存内存地址,该地址存放的值必须是int型的数据。
- *p 获取指针变量 p 指向的内存地址所存放的数据。
#include<stdio.h>
#include<stdlib.h>
int main(){
int i;
i = 10;
//打印变量的地址,注意占位符使用%#x
//printf("%#x\n",&i);
int b = 3;
//定义一个int类型的指针,存放是int类型变量的地址。
//这是一个一级指针
int *p = &b;
//定义一个二级指针,存放的是指针的地址。
int **q = &p;
printf("p指针所存放内存地址对应的值是%d\n",*p);
printf("q指针所存放内存地址对应的值是%d\n",**q);
//指针的常见错误
//常见错误1:野指针
int *pp ;
//定义了,但没有赋值的指针,称为野指针。
//printf("pp的内存地址是%#x\n",&pp);
int a = 3;
//在这里直接将a变量赋值给一个指针变量的指向的内存单元。这是不行的,因为这是野指针。
//*pp = a;
//正确做法应该是:给指针pp赋值,让其指向一个有效的内存地址。
pp = &a;
printf("%d\n",*pp);
//常见错误2:指针类型错误
double d = 3.14;
//给指针变量赋值错了数据类型的地址。
//pp = &d;
return 0;
}
指针的长度
-
在 32 位操作系统中,指针的长度是 4 个字节,在 64 位操作系统中,指针的长度是 8 位。
-
那为什么还要给指针分不同的类型呢?
因为在做指针位移时,需要计算指针位移运算。
多级指针&多级指针的访问
多级指针的访问引用传递
#include<stdio.h>
//引用传递
void swap(int *a ,int *b){
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10;
int b = 20;
swap(&a,&b);
printf("a = %d\n",a);
printf("b = %d\n",b);
return 0;
}
主函数获取子函数的变量的内存地址
#include<stdio.h>
//错误版本
//void function(int* p){
//
// int i = 3;
// p = &i;
// printf("i 的地址是%p\n",&i);
//}
//i 的地址是0x7fff4fda8994
//mainp指针存放的地址是:0x7fff4fda89e8
//int main() {
//
//
// //定义一个可以接收地址的指针
// int* mainp;
//
// function(mainp);
//
// printf("mainp指针存放的地址是:%p\n",mainp);
// return 0 ;
//}
//正确版本
void function(int** p){
int i = 3;
*p = &i;
printf("i 的地址是%p\n",&i);
}
//i 的地址是0x7fff5607a994
//mainp指针存放的地址是:0x7fff5607a994
int main() {
//定义一个可以接收地址的指针
int* mainp;
function(&mainp);
printf("mainp指针存放的地址是:%p\n",mainp);
return 0 ;
}
常见错误
- 指针没赋值就使用,没赋值的指针叫野指针
- 指针类型弄错,给int型指针指定了一个非int型数据的地址
数组
- C语言的数组不检测越界
- 数组的内存地址是连续的空间
- 数组变量名保存的地址就是数组第0个元素的地址
- 内存地址可以加减运算
- 内存地址+1表示右移一个单位
- 指针的长度就是4个字节
- 不同的指针类型的长度都是一样的,分不同类型的指针是在位移时比较方便。
#include <stdio.h>
int main(){
char c[] = "hello";
printf("第8个元素的值是%c\n",c[8]);//c语言的数组是不检测越界。
//数组每一个元素的地址都是连续的。
printf("c[0]的地址%#x\n",&c[0]);//c[0]的地址0xeef9f98a
printf("c[1]的地址%#x\n",&c[1]);//c[1]的地址0xeef9f98b
printf("c[2]的地址%#x\n",&c[2]);//c[2]的地址0xeef9f98c
printf("c[3]的地址%#x\n",&c[3]);//c[3]的地址0xeef9f98d
printf("c[4]的地址%#x\n",&c[4]);//c[4]的地址0xeef9f98e
//打印数组变量名字的地址
printf("c数组变量名字的地址是%#x\n",&c);//c数组变量名字的地址是0xeef9f98a
int data[] = {1,2,3,4,5};
//数组每一个元素的地址都是连续的。整型数据每一个元素之间相差4个字节
printf("data[0]的地址%#x\n",&data[0]);//data[0]的地址0xe538f970
printf("data[1]的地址%#x\n",&data[1]);//data[1]的地址0xe538f974
printf("data[2]的地址%#x\n",&data[2]);//data[2]的地址0xe538f978
printf("data[3]的地址%#x\n",&data[3]);//data[3]的地址0xe538f97c
printf("data[4]的地址%#x\n",&data[4]);//data[4]的地址0xe538f980
//打印数组变量名字的地址
printf("data数组变量名字的地址是%#x\n",&data);//data数组变量名字的地址是0xe538f970
//打印的数组变量和打印数组变量的地址是一样的,都是表示数组第一个元素的地址。
printf("data数组变量是%#x\n",data);//data数组变量名字的地址是0xe538f970
//数组变量名字的地址就是数组第一个元素的地址
//内存地址是可以进行加减运算的。
//定义一个 cp 指针,存放数组cy首元素的地址
//指针+1表示内存地址位移一位
char* cp = &c;
printf("第1个元素的值%c\n",*(cp+0));//h
printf("第2个元素的值%c\n",*(cp+1));//e
printf("第3个元素的值%c\n",*(cp+2));//l
printf("第4个元素的值%c\n",*(cp+3));//l
printf("第5个元素的值%c\n",*(cp+4));//o
int* datap = &data;
printf("第1个元素的值%d\n",*(datap+0));//1
printf("第2个元素的值%d\n",*(datap+1));//2
printf("第3个元素的值%d\n",*(datap+2));//3
printf("第4个元素的值%d\n",*(datap+3));//4
printf("第5个元素的值%d\n",*(datap+4));//5
}
数组与指针的关系
如何用指针表示数组
int a[] = {1,2,3,4};
//使用指针p存放数组a首个元素的地址。
int *p = a;
如何通过指针访问数组元素
//使用指针修改数组的值
*(p+0) = 10;
示例代码
#include<stdio.h>
int main() {
int a[] = {1,2,3,4};
//使用指针p存放数组a首个元素的地址。
int *p = a;
printf("数组变量:%p\n",a);//0x7ffeede7d960
printf("指针地址:%p\n",p);//0x7ffeede7d960
//使用指针修改数组的值
*(p+0) = 10;
printf("指针访问数组元素:%d\n",*(p+0));
return 0;
}
常见错误
- 指针没赋值就使用,没赋值的指针叫野指针
- 指针类型弄错,给int型指针指定了一个非int型数据的地址
#include<stdio.h>
int main() {
int * p;
int a = 1;
double d = 4.0;
//printf("%p\n",&p);//指针没有指向不能轻易使用,它是一个野指针
p = &a;
printf("*p = %d\n",*p);//*p = 1
//指针指向地址存放的类型必须和声明指针的类型一致
//p = & d;
return 0;
}
堆与栈
栈
- 栈是自动分配,自动释放
- 大小固定
- 从栈分配的内存空间叫静态内存
堆
- malloc或者new申请
- c语言需要用free释放
- 从堆分配内存空间叫动态内存
- realloc 是在原来的基础上再分配内存空间
malloc
- 动态申请内存空间
- 一个malloc申请出来的内存地址是连续的
- malloc(需要申请的字节数) 返回一个指针
malloc 的图解
malloc 的图解示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
int len ;
printf("请输入数组的长度\n");
scanf("%d",&len);
//malloc动态分配内存,p指针指向首字节地址
int *p = (int*)malloc(sizeof(int)*len);
for(int i = 0;i<len;i++){
*(p+i) = i*10;
}
for(int i = 0;i<len;i++){
printf("第%d个数据是%d\n",(i+1),p[i]);
}
printf("p指针变量的地址:%p\n",p);
printf("第1个元素的值:%d\n",*(p));
int a = p[1];
printf("malloc 分配的内存空间指针相当于一个数组,%d\n",a);
//释放在堆中动态分配的内存空间;
//free()释放的是指针指向的内存。指针变量依然存在,只有程序结束时才被销毁。不过现在指针指向的内容是未定义的垃圾,所以现在指针又成为了野指针。
//void free(void *FirstByte):该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。
free(p);
printf("p指针变量的地址:%p\n",p);
printf("第1个元素的值:%d\n",*(p));
for(int i = 0;i<len;i++){
printf("第%d个数据是%d\n",(i+1),p[i]);
}
/**
在 Mac OS 系统:
free(p) 之后 *P 没有变化,p的地址没有发生变化
在 Linux 系统:
free(p) 之后 *p 的值不再是 free 之前的值
*/
return 0;
}
realloc
重新分配主存
示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
int len ;
printf("请输入数组的长度\n");
scanf("%d",&len);
//malloc动态分配内存,p指针指向首字节地址
int *p = (int*)malloc(sizeof(int)*len);
for(int i = 0;i<len;i++){
*(p+i) = (i+1)*10;
}
for(int i = 0;i<len;i++){
printf("第%d个数据是%d\n",(i+1),p[i]);
}
for(int i = 0;i<len;i++){
printf("指针p[%d]的地址是:%p\n",(i+1),(p+i));
}
int size = 0;
printf("请输入需要补充的值\n");
scanf("%d",&size);
p = (int *)realloc(p,size+len);
for(int i = 0;i<len+size;i++){
*(p+i) = (i+1)*10;
}
for(int i = 0;i<len+size;i++){
printf("第%d个数据是%d\n",(i+1),p[i]);
}
for(int i = 0;i<len+size;i++){
printf("指针p[%d]的地址是:%p\n",(i+1),(p+i));
}
free(p);
return 0;
}
/*
realloc函数的使用
函数名: realloc
功 能: 重新分配主存
用 法: void *realloc(void *ptr, unsigned newsize);
在指针 ptr 后面分配新的内存空间。
请输入数组的长度
4
第1个数据是10
第2个数据是20
第3个数据是30
第4个数据是40
指针p[1]的地址是:0x7fd4e7402ab0
指针p[2]的地址是:0x7fd4e7402ab4
指针p[3]的地址是:0x7fd4e7402ab8
指针p[4]的地址是:0x7fd4e7402abc
请输入需要补充的值
3
第1个数据是10
第2个数据是20
第3个数据是30
第4个数据是40
第5个数据是50
第6个数据是60
第7个数据是70
指针p[1]的地址是:0x7fd4e7402ab0
指针p[2]的地址是:0x7fd4e7402ab4
指针p[3]的地址是:0x7fd4e7402ab8
指针p[4]的地址是:0x7fd4e7402abc
指针p[5]的地址是:0x7fd4e7402ac0
指针p[6]的地址是:0x7fd4e7402ac4
指针p[7]的地址是:0x7fd4e7402ac8
*/
自定义类型
结构体
- 结构体的定义
//1.定义一个结构体
struct Student {
int age;
char *name;
char sex;
//eatp表示函数指针的名字
void(*eatp)();
};
- 结构体成员的赋值
//方式1
struct Student st = {
11,"张三",'m'
};
//方式2
//给结构体内的成员赋值
st.age = 12;
st.name = "李四";
st.sex = 'm';
- 结构体函数指针
结构体内指针不能定义函数,只能定义函数指针。
//给结构体指针赋值
st.eatp = eat;
- 结构体成员的使用
//用结构体变量引用变量
printf("年龄:%d\n名字:%s\n性别:%c\n",st.age,st.name,st.sex);
//用指针变量的方式引用结构体属性
struct Student *p = &st;
printf("年龄:%d\n名字:%s\n性别:%c\n",(*(p)).age,(*(p)).name,(*(p)).sex);
#include<stdio.h>
void eat() {
printf("学生吃饭\n");
}
struct Student {
int age;
char *name;
char sex;
//eatp表示函数指针的名字
void(*eatp)();
};
int main(void){
//定义一个结构体变量,并给结构体成员赋值
// struct Student st = {
// 11,"张三",'m'
// };
struct Student st ;
//给结构体内的成员赋值
st.age = 12;
st.name = "李四";
st.sex = 'm';
//给结构体指针赋值
st.eatp = eat;
//输出结构体成员的值
printf("年龄:%d\n名字:%s\n性别:%c\n",st.age,st.name,st.sex);
struct Student *p = &st;
printf("年龄:%d\n名字:%s\n性别:%c\n",(*(p)).age,(*(p)).name,(*(p)).sex);
printf("年龄:%d\n名字:%s\n性别:%c\n",p->age,p->name,p->sex);
//调用结构体指针。
p->eatp();
return 0;
}
文件处理
- 文件处理流程
- 打开文件
- 文件的处理
- fwrite写入
- fread读取
- rewind 将文件游标指向文件的开头
- 关闭文件
示例代码
//文件的操作
#include<stdio.h>
int main(void){
//定义一个文件类型的变量
FILE *file = NULL;
//打开文件
//FILE *fopen(char *filename, char *type);
//a+ 表示 append 追加 +表示如果没有这个文件就创建一个文件
file = fopen("hello.txt","a+");
char *str = "hello world";
//int fwrite(void *ptr, int size, int nitems, FILE *stream);
fwrite(str,1,11,file);
//将文件指针重新指向一个流的开头
//int rewind(FILE *stream);
//因为文件的打开mode是 a+ 追加的方式,所以要将处理文件的指针指向一个流的开头。才能读取到数据
rewind(file);
char buf[1024] = {};
//读出来
// int fread(void *ptr, int size, int nitems, FILE *stream);
fread(buf,1,11*2,file);
printf("读出来的数据是%s\n",buf);
fclose(file);
return 0;
}
/*
"r" = "rt"
打开一个文本文件,文件必须存在,只允许读
"r+" = "rt+"
打开一个文本文件,文件必须存在,允许读写
"rb"
打开一个二进制文件,文件必须存在,只允许读
“rb+”
打开一个二进制文件,文件必须存在,允许读写
"w" = “wt”
新建一个文本文件,已存在的文件将被删除,只允许写
"w+" = "wt+"
新建一个文本文件,已存在的文件将被删除,允许读写
“wb”
新建一个二进制文件,已存在的文件将被删除,只允许写
“wb+”
新建一个二进制文件,已存在的文件将被删除,允许读写
"a" = "at"
打开或新建一个文本文件,只允许在文件末尾追写
"a+" = "at+"
打开或新建一个文本文件,可以读,但只允许在文件末尾追写
“ab”
打开或新建一个二进制文件,只允许在文件末尾追写
“ab+”
打开或新建一个二进制文件,可以读,但只允许在文件末尾追写
对于文件使用方式有以下几点说明:
1) 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:
r(read): 只读
w(write): 只写
a(append): 追加
t(text): 文本文件,可省略不写
b(binary): 二进制文件
*/
C语言的编译器
编译命令
编译 c 代码,不同的操作系统使用的编译工具不一样,Mac OS 使用的 clang ,Linux 使用的是 gcc
gcc/clang -g -o main main.c -I 关联的头文件位置 -L 关联的库位置 -l关联的库名字 -l关联的库名字2...
编译过程
.c (预处理) -> .i 文件(编译) -> .s(汇编) -> .o(链接) -> 可执行文件
预处理
获取预处理后的.i文件
- 编写一个 helloworld.c 文件
- gcc 进行编译
- gcc -o helloworld.i helloworld.c -E 在当前目录下得到一个 hellowold.i 文件
helloworld.i文件
查看这个文件可以知道,在 .c 预处理之后的 helloworld.i 文件可以看出,源码会被保留,并且会将头文件的内容写到出头文件的内容上面。
- 编译关联库的c文件为add.out
clang -c add.c -o add.out
- 编译静态库
将 add.out 编译为静态库 mylib
libtool -static -o libmylib.a add.out
- 编译带有main函数的main.c文件
clang -o main main.c -I . -L . -lmylib
- 执行最终编译后的文件
./main
源代码如下
- main.c
#include<stdio.h>
#include "add.h"
int main(void){
int result = add(1,2);
printf("输出结果是%d\n",result);
return 0;
}
- add.h
int add(int a,int b);
- add.c
#include<stdio.h>
#include "add.h"
int add(int a,int b){
return a+b;
}
宏定义
就是将 #define 定义的内容安装字符串进行替换。
#include<stdio.h>
//宏定义是没有c语言语法规则的,因此可以不需要写;
#define R 10
#define _main int main(
//传递参数
#define N(n) n*10
//定义一个加法运算的宏
//对比一个函数来说,这样定义宏的好处是什么?
//1.它的参数的类型不限制
//2.返回值类型不限制
#define ADD(a,b) a+b
_main) {
int a = R;
printf("a = %d\n",a);
int b = N(5);
printf("b = %d\n",b);
int c = ADD(5,6);
printf("c = %d\n",c);
float d = ADD(5,6.3);
printf("d = %f\n",d);
//注意:宏定义只是将字符串进行替换操作,不会考虑一些逻辑运算符关系的,需要手动控制。
//2+3*4+5 = 2+12+5 = 19
int e = ADD(2,3)*ADD(4,5);
int f = (ADD(2,3))*(ADD(4,5));
printf("e = %d\n",e);//19
printf("f = %d\n",f);//45 正确的
return 0;
}
类型定义 typedef
一般会是用于给自定义类型起别名,例如在jni中,就大量的使用了typedef来将c的类型起别名为java类型。
typedef int tni;
int main() {
//在预处理后的.i文件,这里的tni并不会像宏定义一样被替换。
tni a = 10;
printf("a = %d\n",a);
return 0;
}
联合体
- 可以将不同类型的变量定义在同一块内存空间中,并且同一时间只有一个变量会被存储。
- union 关键字
#include<stdio.h>
//1.定义一个联合体data
union data {
int a;
int b;
char c;
};
int main() {
union data _data;
_data.a = 1;
_data.b = 2;
_data.c = 'c';
printf("a = %d\n",_data.a);
printf("b = %d\n",_data.b);
printf("c = %c\n",_data.c);
//取出地址,每一个变量存储的地址都是一样的
//%p打印地址
printf("a = %p\n",&_data.a);//0x7fff54c929c8
printf("b = %p\n",&_data.b);//0x7fff54c929c8
printf("c = %p\n",&_data.c);//0x7fff54c929c8
//这里只会输出最后一个赋值的元素 也就是char c = 'c' 会被存储、其他两个会被 c 覆盖
printf("a = %c\n",*(&_data.a));//99
printf("c = %c\n",_data.c);//99
return 0;
}
链表
静态链表
- 预先定义好一些元素通过指针的方式连接起来的一条链子就是链表。
- 先进后出
#include<stdio.h>
struct weapon {
int price;
int atk;
struct weapon * next;
};
int main() {
struct weapon a,b,c,*head;
a.price = 100;
a.atk = 101;
b.price = 200;
b.atk = 201;
c.price = 300;
c.atk = 301;
//head指向第一个元素
head = &a;
a.next = &b;
b.next = &c;
//最后一个元素的next属性为空
c.next = NULL;
struct weapon *p;
p = head;
while(p!=NULL){
printf("price = %d,atk = %d\n",p->price,p->atk);
//下面两种方式都行
//p = (*p).next;
p = p->next;
}
return 0;
}
动态链表
动态的往链表中添加数据
#include<stdio.h>
//mac下<malloc.h>头文件找不到,具体原因未查明
#include<mm_malloc.h>
struct weapon {
int price;
int atk;
struct weapon *next;
};
struct weapon * create() {
struct weapon *head;//指向第一个元素
struct weapon *p1,*p2;//p1指向当前输入的元素,p2指向上一个元素
int n = 0;//表示输入的元素的次数
p1 = p2 = (struct weapon*)malloc(sizeof(struct weapon));
//p1->atk = 10;
//p1指向的是一个 weapon 结构体
//我们向往这个结构体里面的属性赋值,首先就需要取出结构体的元素的地址: &(p->price)
//下面两种输入方式都可以
scanf("%d,%d",&(p1->price),&p1->atk);
head = NULL;
while (p1->price!=0) {
n++;
if(n == 1){
//第一次输入,保存到head中
head = p1;
}else{
p2->next = p1;
}
//p2指向上一个元素(相对下一次循环来说的)
p2 = p1;
//给p1重新分配内存空间
p1 = (struct weapon*)malloc(sizeof(struct weapon));
scanf("%d,%d",&p1->price,&p1->atk);
}
p2->next = NULL;
return head;
}
int main(){
struct weapon * head = create();
while (head!=NULL) {
printf("price = %d,atk = %d\n",head->price,head->atk);
//下面两种方式都行
//p = (*p).next;
head = head->next;
}
return 0;
}
工具
我们在使用 c 语言
开发时,不可能将所有的 API
都记住,因此我们应该常备 API
文档在身,在 Mac
平台比较好用的应该就是 Dash
了。
Dash
可以下载不同语言的 API
文档,非常方便,推荐大家使用.