第一个程序
|
|
基本语法
分号
注释
//
/**/
标识符
以字母下划线开始 以数字字母下划线结束 且区分大小写
C语言数据类型
类型
整型
char 1byte -128到127 或 0到255
unsigned char 1byte 0到255
signed char 1byte -128到127
int 2或4byte -32768到32767 或 -2147483648到2147483647
unsigned int 2或4byte 0到65535 或 0 到4294967295
short 2byte -32768到32767
unsigned short 2byte 0到67735
long 4byte -2147483648到2147483647
unsigned long 4byte 0到429496295
有符号范围: -2^(n-1)~2^(n-1)-1
例如:char 为8位可以表达256个数,分为负数、0、正数三部分,0占用1个数,那么势必一边128个,一遍127个,由于我们使用补码,那么就是负数比正数多一个
这么多整型,如何选择?
没有特别需求,整型通常直接选用int
不是迫不得已,也不用unsigned
整数越界
整数是以纯二进制方式进行计算的,所以:
11111111+1 => 100000000 = 0 越界抛弃第一个1 剩下00000000得到0 11111111是负数,补码符号位外取反+1 得10000001 = -1
![20170807150203834865401.png](http://pic.aipp.vip/20170807150203834865401.png)
char a = 127; // 01111111
char b = a+1; //-128 10000000
8进制和16进制
一个以0开始的数字字面量是8进制 012
一个以0x开头的数字字面量是16进制 0x12
整数的输入输出
10进制输入输出
%d int
%u unsigned
%ld long long
%lu unsigned long long 通常用于输出类型的长度,如printf("%lu",sizeof(int));
%l 打印long类型的值
%u 打印unsigned类型的值
%o 8进制输入输出
%#o 8进制输入输出,带前缀
%x 16进制输入输出
%#x 16进制输入输出,带前缀
%p 指针
%c 以字符形式输出
字符计算
字符也是整型,可以直接运算
1234
char a = 'A';char b = 'B';printf("%d",a-b);printf("%c",++a)
浮点型
float 4byte 1.2e-38 到3.4e+38 7位有效数字(7位之外不再准确) 单精度浮点型
double 8byte 2.3e-308到1.7e+308 15位有效数字 双精度浮点型
long double 10byte
浮点数的输入输出
%f 在%和f中间加上.n可以指定输出几位小数,这样的输出是做4舍5入的
%lf
%e 科学计数法
float a = 1.1;
printf("a=%.3f",a);
浮点数判断是否相等
fabs(f1-f2)<e1-12
这么多浮点型,如何选择?
没有特别需求,直接用double
10.0和10是完全不同的数
带小数点的字面量是double,而不是float,如果想要表示float,需要后跟f 如1.1 和 1.1f
逻辑型
bool
使用#include <stdbool.h>后就可以使用bool和true/false
void类型
void指定没有可用的值,通常有以下三种情况
1.函数不返回值
例如void exit(int status)
2.函数参数为空
例如int rand(void)
3.指针指向void
类型为void的指针代表对象的地址,而不是类型。
指针类型
派生类型
数组类型
结构类型
共用体类型
函数类型
使用
sizeof 是一个运算符,给出某个变量或类型在内存中占用的字节数.需要注意sizeof是静态运算符,它的结果在编译时刻就决定了
int a=6;
printf("sizeof(int)=%ld\n",sizeof(int));
printf("sizeof(a)=%ld\n",sizeof(a++));
printf("a=%ld",a); //不要再sizeof的括号里做运算,这些运算不会真正执行
类型占用空间大小跟平台及编译环境有关
所有的数据在计算及内部都是二进制
整数是如何表达的?
负整数时,使用补码表示 除符号位,其他取反+1
正整数时,使用原码表示
强制类型转换
(类型)值
int->unsigned int->long->unsigned long->long long ->unsigned long long ->float->double->long double
对于printf,对于任何小于int的类型会被自动转换成int;float会被转化成double
对于scanf,无法自动转换,比如要输入short,需要%hd
printf("%d",(short)32768); //-32768 1000 0000 0000 0000 负数+除符号位外取反加1 1000000000000000 最终是-32768
printf("%d",(char)32768); //0 最后8位是00000000 所以是0
C变量常量
变量定义 声明
定义
<变量类型> <变量名称>
int price;
int amount;
变量名是一种标识符,标识符由数字字母下划线构成,标识符不能以数字开头且C语言关键字也不能用过标识符。
定义变量系统已经为该变量分配了内存
声明
变量声明向编译器保证变量以给定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步编译。变量声明只有在编译时有它的意义,在程序连接时编译器需要实际的变量声明。
extern声明在其他文件中的变量
*声明为变量创建了内存空间并指定了初始值*
定义与声明的区别
定义时创建并为该变量分配了内存空间,声明没有分配内存空间
函数声明
int func(void); //先声明再使用
int main(){
int i = func();
}
int func(void){
return 0;
}
建议函数定义或声明时写参数,如果没有参数,就写void
C语言不允许函数嵌套定义
return 数字 数字的范围是0~255,如果return -1 那么结果是255
http://blog.csdn.net/qq_21411985/article/details/44082831
例如
int i,j,k; //定义变量i,j,k
char c,ch;
flaot f,salary;
double d;
int i=3,j=5; //定义并初始化i j
extern int a; //声明外部变量a
常量
定义常量
两种方式定义常量
#define <常量名> <常量值>
const <类型> <常量名> = <常量值>;
例
#define LENGTH 10
const int LENGTH = 10;
常量分类
整数常量
整数常量可以是十进制、八进制或十六进制。
0x或0X开头表示十六进制,,0开头表示八进制,不带前缀则默认表示十进制。
整数常量也可带一个后缀,后缀是U和L的组合,U表示无符号整数(unsigned),L表示长整数(long)。后缀可以是大写,也可以是小写,U和L的顺序任意
例
0772 //八进制
212 //十进制
0x4b //十六进制
30u //unsigned十进制
30l //长整型
30ul //无符号长整型
浮点常量
3.141592
314159e-5L //指数形式
字符常量
转义序列(逃逸字符)
\ \字符
\' '字符
\" "
\? ?
\b 退格键
\f 换页符
\n 换行符
\r 回车
\t 水平制表符
\v 垂直制表符
\ooo 一到3位的八进制数
\xhh 一个或多个十六进制数
C存储类
说明: 存储类定义C程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们锁修饰的类型之前。
C程序中可用的存储类
auto
register
static
extern
auto存储类
说明: auto存储类是所有局部变量默认的存储类
{
int mount;
auto int month; //auto只能用在函数内,即auto只能修饰局部变量
}
register存储类
说明: register存储类用于定义存储在寄存器中而不是RAM中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的'&'运算符(因为它没有内存位置)
寄存器只用于需要快速访问的变量,比如计数器,还应注意,定义register并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
{
register int miles;
}
static存储类
说明: static存储类指示编译器在整个程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用static修饰局部变量可以在函数调用之间保持局部变量的值。
static修饰符也可以应用于全局变量。当static修饰全局变量时,会使变量的作用域限制在声明它的文件内。
在C编程中,当static用于在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
#include <stdio.h>
void func(void);
static int count = 5;
main(){
while(count--){
func();
}
return 0;
}
//定义函数
void func(void){
static int i = 5;
i++;
printf("i is %d and count is %d \n",i,count);
}
extern存储类
extern存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都可见的。当您使用extern时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用extern来得到已定义的变量或函数的引用。
可以这么理解,extern是用来在另一个文件中声明一个全局变量或函数
extern修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候
//main.c文件
#include <stdio.h>
extern void func(); //声明来自全局函数func的引用
int count = 10; //定义全局变量count
main(){
func();
}
//support.c文件
#include <stdio.h>
extern int count; //来自全局变量的引用
void func(){
printf("来自support.c文件%d\n",count);
}
C运算符
算术运算符
+ - * / % ++ --
关系运算符
== != > < >= <=
逻辑运算符
! && || 优先级从高到低
位运算符
假设A为60
& 与
| 或
^ 异或 两位相等为0 不相等为1
~ 翻转 01翻转
<< 左移运算 右侧补0 A<<2将得到240
>> 右移运算 对于unsigned类型,左边填0 A>>3将得到15
signed类型左边填入原来最高位
应用
按位与&
1.让某一位或某些位为0: x & 0xFE
![2017082215033901647719.png](http://pic.aipp.vip/2017082215033901647719.png)
2.取一个数中的一段: x & OxFF
![20170822150339027143479.png](http://pic.aipp.vip/20170822150339027143479.png)
例:
//以二进制形式输出你所输入的整数
int number;
scanf("%d",&number);
number = 0x5555555;
unsigned mask = 1u<<31;
for(;mask;mask>>1){
printf("%d",number&mask?1:0);
}
printf("\n");
按位或|
1.让某位或某些位为1: x|0x01
2.把两个数拼起来: 0x00FF|0xFF00
按位取反~
按位异或^
1.对一个变量用同一个值异或两次,等于什么也没做
2.如果x和y相等,那么x^y的结果为0
左移运算
1.x<<=1 等于 x*=2
x<<=n 等于 x*=n
右移运算
1.x>>1 等于 x/=2
x>>n 等于 x/=n
赋值运算符 结合顺序自右向左
=
+=
-=
*=
、=
%=
<<=
<<=
&=
^=
|=
杂项运算符
sizeof() 返回变量大小
& 获取指针
* 取出指针对应的值
?: 条件运算符
1
printf("%d")
C中运算符优先级
C判断
if
if..else
switch
C循环
while
for
do...while
break
continue
goto
C函数
定义
return_type function_name(parameter list){
//函数体
}
声明
return_type function_name(parameter list);
int max(int,int)
声明的目的是告诉编译器函数长什么样
函数声明可以不写参数名,但是通常会写上
函数参数
形参 实参
**C语言中参数传递是值传递**
返回值
C语言不允许直接返回一个数组作为函数的返回值
不允许返回局部变量的指针,因为局部变量在函数返回后已经作废了
如果要返回指针,可以是全局变量或静态变量
int intsum(int,int); //声明
printf("%d",intsum(1,2));
int intsum(int a,int b){ //定义
return a+b;
}
*返回指针的函数
返回局部变量的地址是危险的
返回全局变量或静态变量的地址是安全的
返回在函数内malloc的内存是安全的,但是容易造成问题
最好的做法是返回传入的指针
参数类型自动转换
C语言中存在一个很大的问题,就是参数类型会自动转换
void cheer(int a){
printf("%d",a);
}
int main(){
cheer(1.1); //虽然期望的是int类型,但是传递1.1同样通过,会被自动转换成int
}
C作用域
局部变量
函数的每次运行,就会产生一个独立的变量空间,在这个空间中的变量,是这次函数运行所独有的,称作局部变量。
定义在函数内部的变量就是局部变量
全局变量
全局变量在定义后的代码中可直接使用,包括函数(这一点和php不同,php中函数中不能直接使用全局变量,需要使用global关键字声明)
初始化局部变量和全局变量
数组
number[0] number[1]
数组一旦创建,不能改变大小
所有的数组都是由连续的内存位置组成,最低的地址对应第一个元素,最高的地址对应最后一个元素
数组名是一个指向数组中第一个元素的常量指针
number 等于 &number[0]
声明数组
类型 数组名[元素数量]
double balance[10];
初始化数组
double balance[5] = {1.0,2.0,3.0,7.0}; //剩余元素默认值为0
double balance[] = {1.0,2.0,3.0};
balance[4] = 30.0;
int a[13]={1}; //第一个元素为1,其他补0
//二维数组
int arr[][3] = { //二维数组列号不能省略
{1,2,3},
{4,5} //默认补0
}
//多维数组
int arr[2][3] = {
{1,2,3},
{4,5,6}
}
等价于
int arr[2][3] = {1,2,3,4,5,6}
int ele2 = arr[2][2]; //数组元素访问
arr[2] = *(arr+2) //利用首元素指 针位移获取指定元素指针,再取值
数组的指针
数组名就是指向数组首元素的指针,也就是通常意义上数组的指针
传递数组给函数
方式1 形参是一个指针
void myFunction(int *param){}
方式2 形参是一个已定义大小的数组
void myFunction(int param[10])
方式3 形参是一个未定义大小的数组
void myFunction(int param[])
从函数返回数组
方式: 返回数组的指针
多维数组的地址
二维数组a[3][4] a等于a[0]等于&a[0][0] a[0]是第一个一维数组的首地址
p是二维数组的指针变量,若p指向第一个元素a[0][0],则p[i-1][j-1]的值为
*(*(p+i)+j)
指向多维数组的指针变量
type (*variable_name)[length]
char (*arr)[3]
length表示二维数组分解为多个一维数组时,一维数组的长度
http://c.biancheng.net/cpp/html/79.html
http://blog.csdn.net/hanghangde/article/details/50371268
数组越界
程序员有责任保证数组不会越界
在c99中可以通过输入一个整数动态生成整数个元素的数组
int i;
scanf("%d",&i);
int count[i];
...
数组运算
获取数组长度
int a[] = {1,2,3};
printf("%d",sizeof(a)/sizeof(a[0])); //得到数组有多少个元素 sizeof(a) 获取a数组占用的单元数 sizeof(a[0])a中一个元素占用的单元数
遍历数组
int arr[] = {1,2,3};
int length = sizeof(arr)/sizeof(arr[0]);
for(int i=0;i<length;i++){
printf("%d\n",arr[i])
}
//构造一个函数,返回数字在数组中的位置
int search(int key,int arr[],int count){ //注意这里count必须传入,不能在内部使用sizeof(arr)/sizeof(arr[0])获取
test_sizeof的返回值为4,不是16,说明在计算test数组的大小时,该操作数并不是在main函数中定义的map数组,因为sizeof是编译时就计算出结果,此时的test_sizeof函数只知道test是个int 型的数组,所以它的返回值应该是固定的,而与其参数大小无关。那么test_sizeof函数返回值的大小是多少呢,答案是:与指针的大小相同。因为数组名其实与指针是类似的,所以在编译的计算时它的值应该与指针的大小相同,为4个字节(32位)。
1234567891011121314151617181920
int test_sizeof(int test[]);int main(){ int map[] = {1, 2, 3, 4}; printf("main::sizeof(int) = %d\n", sizeof(int)); printf("main::sizeof(map) = %d\n", sizeof(map)); printf("test_sizeof::sizeof(map) = %d\n", test_sizeof(map)); return 0;}int test_sizeof(int test[]){ return sizeof(test);}
int ret = -1;
for(int i = 0;i<count;i++){
if(key==arr[i]){
ret = i;
break;
}
}
return ret;
指针
知识:
指针是一个变量,其值是另一个变量的地址,即内存位置的直接地址
> void指针是通用指针。void指针可以转换为除函数外的任何指针
声明
type *var-name
int *ip; //整型指针
double *dp; //double指针
char *ch; //字符型指针
各数据类型的指针的唯一不同就是指针所指向的变量或常量的数据类型不同
int i;
int *p=&i; //变量p中保存的值就是变量i的地址,那么我们就说是p指向了i
int* p=&i; //与上面一句相等
int* p,q; //这里q只是一个普通int类型变量
取地址和取值
&取地址
&右边必须是变量
取一个变量的地址,如何正确输出地址?
int i=0;
int j=0;
printf("%p\n",&i); //%p用16进制形式输出内存地址。 (通过程序显示的地址是在操作系统虚拟内存映射下的地址,并非真正的物理地址)
printf("%p\n",&j); //输出i和j的地址,会发现正好相差4个字节,32位,也说明它俩在内存中位置是挨着的
c语言内存模型是堆栈的形式,局部变量和函数上下文在栈中,栈是自顶向下的,所以先定义的变量地址更高,后定义的变量地址更低
![20170814150265099513160.png](http://pic.aipp.vip/20170814150265099513160.png)
//取数组的地址
int a[10];
printf("%p\n",&a); //0x7fff5d60a710
printf("%p\n",a); //0x7fff5d60a710
printf("%p\n",&a[0]); //0x7fff5d60a710
printf("%p\n",&a[1]); //0x7fff5d60a714
由此可见数组中数组名等于数组指针等于数组首元素指针,各元素间顺序排列且间隔N个字节(根据类型不同字节数不同)
*取值
*p可以是左值,也可以是右值
空指针
变量声明时,没有确切的地址可以复制,为指针变量赋值一个NULL值是一个良好的习惯,赋值为NULL的指针被称为空指针
int *ptr = NULL;
printf("%p",ptr); //0x0
printf("%d",ptr); //0
内存地址0有特别重要的意义,它表明该指针不指向一个可访问的内存地址。按照惯例,如果指针包含空值(零),则假定它不指向任何东西
检查空指针
if(ptr)
if(!ptr)
指针与const
指针常量(指向指针的常量)
指针常量指指针本身是一个常量
type * const pointer
int * const ptr;
ptr++; //error
const修饰的是ptr,表示ptr指向的内存地址是const 不可修改
常量指针(指向常量的指针)
从名字中就可看出,一个指针,我们无法修改指针指向的内容,这种指针就叫做常量指针。对于这类指针,你可以修改指针指向的地址,但是不能修改指针指向的内容。
const int *ptr;
const修饰的是int,表示ptr所指的内存总的值是const,不可修改
简单理解const在*前面代表指针常量,const在*后面代表常量指针
const数组
const int a[] = {1,2,3,4}
数组变量已经是const的指针,这里的const标明数组的每个单元都是const int
必须通过初始化进行赋值
保护数组值
如果要向函数中传入一个数组,但是不希望这个数组被修改,那么可以设置这个参数为const
int sum(const int a[],int length)
int f(const int *a); //保护*a不被改变
转换
总是可以把一个非const的值转换成const的
void f(const int * x);
指针的算术运算
C指针是一个用数值表示的地址,因此可以对指针执行算术运算
假设ptr是一个指向地址1000的整型指针,是一个32位的整数,ptr++后指向的地址是1004
常用操作包括
指针加法
指针减法
*p++
取出p所指的数据,完事后顺便把p移到下一个位置
++优先级比*高
常用于数组类的连续空间操作
int main(){
int *ptr
int list[] = {1,2,3,4,5}
ptr = list;
int *ptr2 = &list[4];
//指针加法
printf("%d",*ptr); //1
printf("%d",*(ptr+1)); //2
printf("%d",*(ptr+2-1));//2
//指针减法
printf("%d",ptr2-ptr); //4 表示4个类型单位的距离
return 0;
//*p++
//遍历数组
int arr2[3] = {1,2,3};
int *p2 = arr2;
int size = 0;
size = sizeof(arr2)/sizeof(arr2[0]);
printf("遍历数组\n");
for(i=0;i<size;i++){
printf("%d\n",*p2++);
}
}
指针类型
类型转换
void* 表示不知道指向什么指针
指针的大小
32位系统中指针变量大小为4个字节 64位系统指针变量为8个字节 与指针类型没关系
0地址
NULL是一个预定义符号,表示0地址
通常用0地址表示特殊的事
1.返回的指针是无效的
指针可以转换类型
迷途指针
如果原内存被释放,但是原指针却仍然指向原来区域,那么该指针则被称为迷途指针。迷途指针最好的解决办法就是将释放后的指针置为NULL
函数指针
函数指针可以像一般函数一样,用于调用函数、传递参数。
定义类型
typedef int (*fun_ptr)(int,int); //声明一个类型func_ptr,这个类型是指向参数是int,int、返回值是int的函数的指针
定义变量
int (*p)(int,int) = f; //int f(int a,int b)是一个函数
注意: 在这里&f 和 f一样 函数的名字就是地址 例如max就是max函数的地址
例1:
int (*p)(int,int) = sum;
printf("%p\n",sum);
printf("%p\n",&sum); //等同于上一种方式
printf("%p\n",p);
printf("1+2=%d\n",p(1,2));
printf("1+2=%d\n",(*p)(1,2)); //等同于上一种方式
//结果:
0x1002e4e10
0x1002e4e10
0x1002e4e10
1+2=3
1+2=3
例2:
int max(int x,int y){
return x>y?x:y;
}
int main(void){
//p是函数指针
int (*p)(int,int)=max;
int a,b,c,d;
printf("请输入三个数字:");
scanf("%d %d %d",&a,&b,&c);
d = p(p(a,b),c);
printf("最大的数字是%d\n",d);
printf("%d\n",main);
printf("%d\n",p);
printf("%d\n",max);
return 0;
}
函数指针作为某个函数的参数
void fillarray(int *array,size,int (*generateValue)(void)){
for(int i=0;i<size;i++){
array[i]=generateValue();
}
}
int getValue(){
return rand();
}
int main(void){
int myarray[10];
fillarray(myarray,10,getValue);
for(int i=0;i<10;i++){
printf("%d\n",myarray[i]);
}
printf("\n");
}
示例:
int *a = 1,b=2,*c=&b; //指针变量a为1 整型变量b值为2 定义整型指针变量c 且c的值是变量b的地址
printf("%d",a); //1
printf("%d",b); //2
printf("%d",*c); //2
int size = sizeof(char);
char *arr = (char *)malloc(size*10);
arr[0] = 1;
arr[1] = 2;
arr[1] = 3;
printf("%s",arr); //123
指针与数组
int test(int a[],int *b){
printf("%d\n",a[0]);
printf("%d\n",b[0]);
}
int main(){
int arr[] = {1,2,3};
test(arr,arr); //输出1 1
}
函数参数表中的数组实际上是指针,但是可以用数组的运算符[]进行运算
以下四种函数原型是等价的
int sum(int *ar,int n);
int sum(int *,int);
int sum(int ar[],int n);
int sum(int [],int);
实际上数组变量是特殊的指针,数组变量本身表达地址
int a[10];
int *p=a; //这里不需要使用&
[]运算符可以对数组做,也可以对指针做
p[0]<==>a[0]
int a=10;
int *p=&a;
int arr={1,2,4};
printf("%d\n",*p); //p指向a,所以*p就是a的值,输出10
printf("%d\n",p[0]); //p是地址,[]会把p指向的位置当做是一个数组,所以p[0]输出10
printf("%d\n",a[0]); //报错,变量不可以直接[]运算
printf("%d\n",*arr); //arr是地址,等同于&a[0],*运算获取地址arr[0]处的变量值,得1
数组变量是指针常量,指针地址不能变,所以不能被赋值
int a[]等于int * const a
int a[]={1};
int b[]={2};
a=b; //报错
指针应用场景
1.交换变量
2.函数需要返回多个值
void minmanx(int a[],int len,int *max,int *min);
3.函数返回运算的状态,结果通过指针返回
常用套路是让函数返回状态值如-1或0等
4.需要传入较大的数据时用作参数
5.传入数组后对数组做操作
6.动态申请内存
指针常见错误
1.定义一个指针变量,还没有指向任何变量,就开始使用指针
int i=6;
int *p=0;
int k;
k=12;
*p=12; //不能这么使用,因为p没有指向的变量,它所保存的值是随机的,那么把这个随机值当做是一个地址的话,可能能写入,可能不能写入,所以指针使用之前一定要先指向变量
字符串
理解
C语言中,字符串实际是使用NULL字符'\0'终止的一维字符数组。
计算字符串长度的时候不包含这个'\0'。
不能用sizeof对字符串求长度,使用strlen(string)。
字符串以数组的形式存在,以数组或指针的形式访问。
string.h中有很多处理字符串的函数。
通过数组的方式可以遍历字符串。
字符串字面量不是位于堆区域,也不是栈区域,对于字符串字面量来讲,没有作用域的概念。(""中的字符就是字符串字面量)
初始化
//方式1
char str[] = "hello";
//方式2
char str[13]; //注意不能写成str[],这样程序无法分配内存
strcpy(str,"hello");
//方式3
使用字符指针,动态分配内存
char str[] = "hello";
char *s = (char*)malloc(strlen(str)+1);
strcpy(s,str);
//方式4
使用字符指针,直接指向字符串字面量 (推荐)
char *s = "hello";
定义
char *str="hello"; //常用方式1
char str[]="hello"; //常用方式2
char str[10]="hello";
char str[10]={'h','e','l','l','o','\0'};
例:
int i=0;
char *s="Hello World";
char *s2="Hello World";
char s3[]="Hello World";
printf("&i=%p\n",&i);
printf("s=%p\n",s);
printf("s2=%p\n",s2);
printf("s3=%p\n",s3);
s3[1]='B';
printf("%s",s3);
s[1]='B';
printf("%s",s);
结果:
&i=0x7fff55827718
s=0x10a3d8f7c
s2=0x10a3d8f7c
s3=0x7fff55827722
Bello World
分析:
1.使用char *s="Hello World";会生成一个指针s,初始化指向一个字符串常量
字符串常量会被放在程序的代码段,程序的代码段地址很小(如s、s2),且只读。
在代码段中s、s2指向的值相同,会指向相同的位置。
由于s是只读的,所以s[1]='B'会出错导致程序崩溃
2.如果需要修改字符串,应该使用数组char s[]="Hello World";
![20170816150281510411154.png](http://pic.aipp.vip/20170816150281510411154.png)
那么定义字符串时应该使用指针还是数组?
如果要构造一个字符串->数组
作为本地变量空间自动被回收
如果要处理一个字符串->指针
处理函数参数
动态分配空间
赋值
char s7[10] = "hello";
char s8[10];
// s8 = s7; //字符串变量不能等号复制,需要使用strcpy
strcpy(s8,s7);
printf("%s\n",s8);
字符串的输入输出
char s[4],s2[4];
//方式1 会被空格打断
scanf("%s %s",s,s2);
//方式2
fgets(s,4,stdin);
常见错误
1.指针没有初始化直接使用,后果是指向的位置有可能不可用(并没有开辟空间)
char *s;
scanf("%s",s);
修改后:
char s[100];
scanf("%s",s);
字符串内存分布
//全局变量,位于全局内存
#include<string.h>
char *globalHeader = "HELLO WORLD!";
char globalArray[] = "HELLO WORLD!";
void displayString()
{
//静态变量,位于全局内存
static char *staticHeader = "HELLO WORLD!";
static char staticArray[] = "HELLO WORLD!";
//局部变量,位于自动内存(栈内存)
char *localHeader = "HELLO WORLD!";
char localArray[] = "HELLO WORLD!";
//动态变量,位于动态内存(堆内存)
char *heapHeader = (char *)malloc(strlen("HELLO WORLD!")+1);
strcpy(heapHeader,""HELLO WORLD!"")
}
![20170829150400700090397.png](http://pic.aipp.vip/20170829150400700090397.png)
字符串数组
字符串数组的指针
char *a[] 其中每一个元素都是一个指针
![20170816150287181910370.png](http://pic.aipp.vip/20170816150287181910370.png)
//argc表示*argv这个字符串数组中有argc个字符串
int main(int argc,char const *argv[]){
for(int i=0;i<argc;i++){
printf("%s\n",argv[i]);
}
}
常见错误
1. 字符串数组的指针理解为char **a;
a是一个指针,指向另外一个指针,那个指针又指向一个字符或字符串
字符串与字符
字符串常量用双引号" 字符常量用单引号'
字符在内存中以ASCII码存储,所以可以与整型常量进行运算。
单字符
输入输出
int putchar(int c);
向标准输出写一个字符,返回写了几个字符,EOF(-1)表示写失败
int getchar(void);
从标准输入读入一个字符
返回类型是int是为了返回EOF(-1)
如何结束输入?
windows->ctrl-z
unix->ctrl-d
int ch;
while((ch=getchar())!=EOF){
putchar(ch);
}
printf("EOF\n");
![20170817150290899114926.png](http://pic.aipp.vip/20170817150290899114926.png)
见10.2-1
字符串函数
需要#include <string.h>
size_t strlen(const char *s);
int strcmp(const char *s1,const char *s2);
char * strcpy(char *restrict dst,const char *restrict src);
把src的字符串拷贝到dst
restrict标明src和dst不重叠(C99)
返回dst(为了能链起来)
char * strcat(char *restrict s1,const char *restrict s2);
把s2拷贝到s1的后面
返回s1
s1必须又足够空间
char * strchr(const char *s,int c);
char * strrchr(const char *s,int c);
//返回NULL表示没有找到
char * strstr(const char *s1,const char *s2);
char * strcasestr(const char *s1,const char *s2);
strcpy/strcat等都会出现越界的问题,建议使用安全版本
char * strncpy(char *restrict dst,const char *restrict src,size_t);
char * strcat(char *restrict s1,const char *restrict s2,size_t n);
int strncmp(const char *s1,const char *s2,size_t n); //只比较前n个字符
strcpy(s1,s2); //复制字符串s2到s1
strcat(s1,s2); //连接字符串s2到s1的末尾
strlen(s1); //返回字符串s1的长度 长度比sizeof(s1)小1
strcmp(s1,s2); //如果s1和s2相同,返回0;如果s1<s2则返回小于0;如果s1>s2则返回大于0
strchr(s1,ch) //返回一个指针,指向字符串s1中字符ch的第一次出现的位置
strstr(s1,s2) //返回一个指针,指向字符串s1中字符串s2的第一次出现的位置
示例:
char s1[12] = "Hello"; 字符串赋值方式1
char s2[12] = "World";
char s3[12];
int len;
strcpy(s3,s1); //字符串赋值方式2
printf("s3: %s\n",s3);
strcat(s1,s2);
printf("strcat s1 s2: %s\n",s1);
len = strlen(s1);
printf("strlen s1: %d",len);
枚举(不常用)
声明
enum 枚举类型名字 {名字0,...名字n} //值从0依次开始
enum COLOR {RED,YELLOW,GREEN,NumCOLORS}; //套路,NumCOLORS记录总共有几种颜色
enum COLOR {RED=1,YELLOW=3} //也可以赋值
void f(enum color c){
printf("%d",c);
}
结构体
声明定义
//方式1 先声明再定义
struct Student{
char name[50];
int age;
};
struct Student student1;
//方式2 声明定义
struct Student{
char name[50];
int age;
}student1;
和本地变量一样,在函数内部声明的结构类型只能在函数内部使用
通常在函数外部声明结构体,这样就可以被多个函数所用
初始化
struct Student s1 = {"xiaoming",18};
访问: 使用成员访问运算符.
结构体大小(参考位域)
https://www.zhihu.com/question/28958350
结构体运算
要访问整个结构,直接用结构变量的名字
对于整个结构,可以做赋值、取地址,也可以传递给函数参数 结构体赋值是值拷贝
p1 = (struct Student){"xiaoming",10}; //相当于p1.name="xiaoming";p1.age=10;
p2=p1 //相当于p2.name=p1.name;p2.age=p2.age;
注意: 数组无法做以上两种运算
指向结构体的指针
和数组不同,结构体变量的名字并不是结构体变量的地址,要取地址必须使用&运算符
//例1
struct date{
int year;
int month;
int day;
}myday;
struct date *p=&myday; //指针使用之前必须初始化
(*p).month=12;
p->month=12;
//例2 动态内存分配
struct Person{
int age;
char *name;
};
char *ptr = "xiaoming";
struct Person *p = (struct Person*)malloc(sizeof(struct Person));
p->age = 10;
p->name = (char*)malloc(strlen(ptr)+1); //结构体成员指针需要初始化
strcpy(p->name,ptr);
//例3 动态内存分配2 效果等同于例2
struct Person{
int age;
char *name;
};
char *ptr = "xiaoming";
struct Person *p = (struct Person*)malloc(sizeof(struct Person)+strlen(ptr)+1);
p->age = 10;
p->name = ptr;
pritnf("name: %s",p->name);
结构体作为函数参数
//方式1 通过指向结构体的指针(常用)
struct date* getStruct(struct date *p){
scanf("%d",&p->year);
scanf("%d",&p->month);
scanf("%d",&p->day);
return p;
}
//方式2 直接值传递 有问题,无法对外部变量做修改
int numberOfDays(struct date d){}
结构体数组
struct date dates[100];
struct date dates[] = {{2017,8,21},{2017,8,22}}
嵌套的结构体
struct point{
int x;
int y;
}
struct rectangle{
struct point pt1;
struct point pt2;
}
![201708211503314564968.png](http://pic.aipp.vip/201708211503314564968.png)
示例
struct Books{
char title[50];
char author[50];
char subject[100];
int book_id;
}book;
book.book_id = 1; //给成员book_id赋值
strcpy(book.title,"mysql"): //给字符串成员赋值
//book.title = "mysql"; //报错,因为book.title是一个常量指针,不能更改常量指针的地址
共用体
定义: 共用体是一种特殊的数据结构,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。公用提提供了一种使用相同的内存位置的有效方式。
共用体占用的内存应足够存储共用体最大的成员。下例中Data将占用20个字节的内存空间。
访问:使用成员访问运算符.
示例
union Data{
int i;
float f;
char str[20];
}
int main(){
union Data data;
printf("Memory size: %d\n",sizeof(data));
data.i = 10;
printf("data.i: %d\n",data.i);
data.f = 1.1;
printf("data.f: %f\n",data.f);
strcpy(data.str,"c programming");
printf("data.str: %s\n",data.str);
return 0;
}
类型定义(typedef)
定义: 可以用它来为类型取一个新的名字,且按照惯例使用大写字母
示例:
typedef unsigned char BYTE;
BYTE b1,b2;
typedef struct Books{
char name[50];
page int;
}Book;
typedef *char[10] Strings; //Strings是10个字符串的数组指针的类型
int main(){
Book book; //等于struct Books book;
strcpy(book.name,"mysql全解");
book.page = 200;
//Book book = {"mysql全解",200};
}
全局变量
初始化
没有做初始化的全局变量会得到0值
指针会得到NULL值
只能用编译时刻已知的值来初始化全局变量
它们的初始化发生在main函数之前
如果函数内部有相同名字的局部变量,那么全局变量会被局部变量覆盖
不要使用全局变量来在函数间传递参数和结果
尽量避免使用全局变量
使用全局变量和静态局部变量的函数是线程不安全的
int gAll;
int main(int argc,char const *argv[]){
printf("%d",gAll); //0
}
静态本地变量
静态局部变量实际上是特殊的全局变量
静态局部变量具有全局的生存期,函数内的局部作用域
int main(int argc,const char *argv[]){
f();
f();
f();
}
sint f(void){
static int all=1;
printf("&gAll=%p\n",&gAll);
all++;
}
位域
定义: 更好的使用内存空间的方式
解析:
1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
示例:
//bs占用8个字节 以类型的存储大小为基本单位 int占用4个字节即32位 a+b=33位大于1个字节 位域不能跨两个存储单位 所以 前4个字节存储a 后4个字节存放b和c
struct{
int a:31; //表示成员a占用31个比特
int b:2;
int :5; //无位域名
int d:2;
}bs;
printf("bs:%d",sizeof(bs)); //bs:8
参考: http://baike.baidu.com/link?url=BH6LQasrA5C0B5pO-b2v3JD8_iwAjGC2DQhS79trY2Nx6G1V-Hm_hWxhhrfus4SgRO4o6Odt3HDE9ONkiF2M8OY-3mI08y72wYfIlXy98pG
输入输出
printf() //%s字符串 %d整数 %c字符 %f浮点数 %lf 双精度浮点 print format 格式化输入
scanf() //scanf碰到空格会停止读取 scan format 格式化输入
getchar() //从屏幕读取下一个可用的字符
putchar() //把字符输出到屏幕
char *gets(char *s); //从stdin读取一行到s所在缓冲区,直到一个终止符或EOF
int puts(const char *s); //函数把字符串s和一个尾随的换行符写入到stdout
经常碰到输出的数据还没输入,下一行要求输入的代码限制性了,这是由于输出缓冲区导致的 ,可以使用fflush(stdout)刷新输出缓冲区
http://blog.csdn.net/u013053957/article/details/46521351
文件读写
简单实现
./test <12.in > result.log 读入12.in的数据 并将输出写入到result.log中
打开文件
FILE *fopen(const char *filename,const char * mode);
mode
r
w
a
r+
w+
a+
如果是二进制文件 需要使用rb/wb/ab/r+b/w+b/a+b来取代
例:
FILE *fp;
fp = fopen("xxx.txt","w+");
if(fp==NULL){
printf("错误码: %d\n错误信息: %s\n",errno,strerror(errno));
perror("错误");
}else{
char ch;
ch = fgetc(fp);
//输出文件中所有字符
while(ch!=EOF){ //或者是 while(!feof(fp)){
putchar(ch);
ch = fgetc(fp);
}
fclose(fp);
}
关闭文件
int fclose(FILE *fp);
成功返回0 错误返回EOF
这个函数会清空缓冲区数据,关闭文件,并释放用于该文件的所有内存。EOF是一个定义在头文件stdio.h中的常量。
写入文件
int fputc(int c,FILE *fp); //写入单个字符
成功返回非负值,错误返回EOF
读取文件
int fgetc(FILE *fp); //读取单个字符
char * fgets(char *buf,int n,FILE *fp); //读取n-1个字符
判定文件结尾
可以判断是否读到EOF,如果是EOF表示已经到达文件末尾
123456789101112131415
//按行读取FILE *fp2;int maxlength = 1000;char strLine[maxlength];fp2 = fopen("/Users/sunxiangke/project/clang/c_new/prod.log","r");if(fp2==NULL){ printf("错误码: %d 错误信息: %s\n",errno,strerror(errno)); perror("错误");}else{ while(!feof(fp2)){ fgets(strLine,maxlength,fp2); printf("%d",strlen(strLine)); } printf("\n");}
]]
预处理器
定义
C预处理器只不过是一个文本替换工具而已,他们会指示编译器在实际编译之前完成所需的预处理。我们把C预处理器称为CPP。
编译过程
使用gcc xxx.c --save-temps编译并保存临时文件,可以看到有.c/.i/.s/.o/a.out文件
编译过程是.c->.i->.s->.o.->a.out
其中.i是预处理后的文件
.o 目标代码文件
常见预处理器指令
#define
#include
#undef 取消已定义宏
#ifdef 如果宏已定义,返回真
#ifndef
#if
#else
#elif
#endif
#error
#pragma
示例:
#ifndef MESSAGE
#define MESSAGE "you wish!"
#endif
#define PI 3.1415
#define PRT printf("%f",PI)
#像函数的宏
#define cube(x) ((x)*(x)*(x))
cube(5)
带参数的宏
定义带参数的宏的原则
一切都要括号
整个值要括号
参数出现的每个地方要括号
#define RADTODEG(x) ((x)*57.39578)
可以带多个参数
#define MIN(a,b)((a)>(b))?(b):(a))
在大型程序代码中非常普遍
注意 不要以;结尾
预定义宏
__DATE__ 一个以MMM DD YYYY格式表示的字符常量 Apr 21 2017
__TIME__ HH:MM:SS格式常量 17:39:06
__FILE__ 当前文件名
__LINE__ 当前行号
__STDC__ 当编译器以ANSI标准编译时,定义为1
头文件
定义
把函数原型放到一个头文件(.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型
说明
加载头文件相当于复制头文件的内容
只有声明(常量、宏、系统全局变量和函数原型声明)可以被放在头文件中,需要的时候随时引用这些头文件。这是规则不是法律
两类头文件
自己编写的头文件
编译器自带的头文件
#include 使用""和<>加载文件
""要求编译器首先在当前目录(.c文件所在目录)寻找这个文件,如果没有,到编译器指定的目录去找
<>让编译器只在指定目录去找
编译器知道自己的标准库头文件在哪
#include <file> //加载系统头文件
#include "file" //加载用户头文件 在当前文件的目录中搜索名为file的文件
对于linux,标准库头文件在/usr/include下
一般情况下,任何.c都有对应的同名.h,把所有对外公开的函数原型和全局变量的声明放进去
不对外公开的函数
在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
#include的误区
#include不是用来引入库的,只是把文件的内容插入到当前行
stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(windows)或.a(linux)中
现在的C语言编译器默认会引入所有的标准库
#include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型
声明
声明是不产生代码的东西
函数原型
变量声明
结构声明
宏声明
枚举声明
类型声明
inline函数
定义是产生代码的东西
只有声明可以放到头文件中
同一个编译单元里,同名的结构不能被重复声明,为了防止重复声明,通常使用标准头文件结构
标准头文件结构
#ifndef __LIST_H__
#define__LIST_H__
#include "node.h"
typedef struct _list{
Node* head;
Node* tail;
} List;
#endif
例
//main.c
#include "max.c"
//max.c
int max(int a,int b){
..
}
//max.h
int max(int a,int b);
extern int i;
注意
int i;是变量的定义
extern int i是变量的声明
错误处理
引入头文件errno.h
使用常量errno
errno //全局错误码
strerror(errno) //获取错误描述
perror("描述") //输出错误信息
#include <errno.h>
int main(){
FILE *fp;
fp = fopen("xxx.txt","w+");
if(fp==NULL){
printf("错误码:
fp = fopen("xxx.txt","w+");
if(fp==NULL){
printf("错误码: %d\n错误信息: %s\n",errno,strerror(errno));
perror("错误");
}else{
fclose(fp);
}
}
程序退出状态
EXIT_SUCCESS
EXIT_FAILURE
内存管理
动态内存管理函数
stdlib.h头文件
calloc
void *calloc(int num,int size); //该函数分配一个带有num个元素的数组,每个元素的大小为size字节,并把他们进行初始化(相当于malloc+memset)
free
void free(void *address); //该函数释放address所指向的内存块
malloc
void *malloc(int num); //该函数分配一个num字节的堆内存空间
realloc
void *realloc(void *address,int newsize); //该函数重新分配内存,把内存扩展到newsize
memcpy
void *memcpy(void *dest, const void *src, size_t n); //拷贝内存
memset
void *memset(void *s, int c, size_t n); //用字符c填充地址s,填充长度为n
常用于清空内存区域
注意:
如果申请失败则返回0,或者叫NULL,所以可以进一步判断
int ptr = (int*)malloc(sizeof(int));
if(ptr!=NULL){
...
}else{
...
}
free释放内存空间后记得将指针赋值NULL
内存泄漏
申请了内存使用后没有正确释放,导致可用内存下降,这就是内存泄漏
malloc和free必须成对出现,否则会造成内存泄漏
定义了动态成员变量的结构体,只释放了结构体指针,而没有释放成员指针,这样也会造成内存泄漏
几款监控调试内存泄漏的工具
Valgrind
Dmalloc
常见问题:
1.申请了没free->长时间运行内存逐渐下降
2.free过了再free
3.地址变了,直接去free
示例:
//例1
float b=3.14,*a=&b;
int *p=(int *)a; //将无类型指针a转换为整型指针
//例2
int number;
int *a;
printf("输入数量: ");
scanf("%d",&number);
//申请内存
a = (int*)malloc(number*sizeof(int)); //malloc返回void * 无类型指针 使用(int*)转换为整型指针
//输入内容
for(int i=0;i<number;i++){
scanf("%d",&a[i]);
}
for(int i=number-1;i>=0;i--){
printf("%d ",a[i]);
}
free(a);
//例3
void *p;
int cnt=0;
while(p=malloc(100*1024*1024)){
cnt++;
}
printf("申请了%dM的空间");
整型指针 字符型指针等 是有区别的,比如正那行指针可以访问4个字节,字符型1个字节
动态内存分配
命令行参数
int main(int argc,char *argv[]){
printf("%d",argc);
printf("%s",argv[1]); //参数从argv[1]开始
return 0;
}
argc是传入参数的数量
argv是一个指针数组
可变数组(resizable array)
//array.h
#ifndef _ARRAY_H_
#define _ARRAY_H_
typedef struct{
int *array;
int size;
}Array;
Array array_create(int init_size);
void array_free(Array *a);
int array_size(const Array *a);
int *array_at(Array *a,int index);
void array_inflate(Array *a,int more_size);
int array_get(const Array *a,int index);
void array_set(Array *a,int index,int value);
#endif
//array.c
const int BLOCK_SIZE=5;
Array array_create(int init_size){
Array a;
a.size = init_size;
a.array = (int*)malloc(sizeof(int)*init_size);
return a;
}
//这里没有选择传入指针然后返回指针的做法,原因是传入数组后,还需要校验指针是否是null,又或者传入的数组已经初始化,那么就需要先free掉
void array_free(Array *a){
free(a->array);
a->array=NULL;
a->size=0;
}
int array_size(const Array *a){
return a->size;
}
//获取指定元素的指针
int* array_at(Array *a,int index){
if(index>=a->size){
array_inflate(a,(index/BLOCK_SIZE+1)*BLOCK_SIZE-a->size);
}
return &(a->array[index]);
}
int array_get(const Array *a,int index){
return a->array[index];
}
void array_set(Array *a,int index,int value){
a->array[index]=value;
}
void array_inflate(Array *a,int more_size){
int *p = (int*)malloc(sizeof(int)*(a->size+more_size));
int i;
for(i=0;i<a->size;i++){
p[i]=a->array[i];
}
free(a->array);
a->array = p; //指针变量之间也是可以相互赋值的
a->size+=more_size;
}
int main(int argc,const char *argv[]){
Array a = array_create(100);
array_free(&a);
return 0;
}
这个可变数组的缺陷
1.每一次变大的时候要申请一份新的内存空间,要花费很多时间去拷贝
2.明明有很多内存,但是却再也不能拷贝了
![20170822150340142116466.png](http://pic.aipp.vip/20170822150340142116466.png)
14-2-1
解决
假如不是每次重新申请内存,而是通过链接的方式将多块BLOCK链起来,问题自然解决
![20170822150340154146994.png](http://pic.aipp.vip/20170822150340154146994.png)
链表
//node.h
#ifndef _NODE_H_
#define _NODE_H_
typedef struct _node{ //内部要使用该结构体,所以给个名字_node
int value;
struct _node *next; //因为编译器读到这里还没有Node,所以不能写成Node *next
} Node;
#endif
//linked-list.c
int main(int argc,const char *argv[]){
Node *head = NULL;
int number;
do{
scanf("%d",&number);
if(number!=-1){
Node *p=(Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
Node *last = head;
if(last){
while(last->next){
last=last->next;
}
last->next=p;
}else{
head=p;
}
}
}while(number!=-1)
}
链表函数
//定义新类型链表List
typedef struct _list{
Node *head;
} List;
int main(int argc,const char *argv[]){
List list;
int number;
list.head = NULL;
do{
scanf("%d",&number);
if(number != -1){
head = add(&list,number);
}
}while(number != -1);
}
//向链表中添加元素
void add(List *pList,int number){
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
Node *last = pList->head;
if(last){ //如果last是NULL,标明当前操作的是是头元素结点
while(last->next){ //last是指针
last=last->next;
}
last->next=p;
}else{
pList->head=p;
}
pList->head = p;
}
借助自定义类型List代表链表的好处是可以自己做各种扩展,例如
typedef struct _list{
Node *head;
Node *tail; //直接记录尾部元素的地址,省掉每次while循环的时间
}
链表的搜索
//查找指定数字是否在链表中
int hasVal(List *pList,int number){
Node *p;
int isFound = 0;
for(p=pList->head;p;p=p->next){
if(p->value==number){
isFound = 1;
break;
}
}
return isFound;
}
void printf(List *pList){
Node *p;
for(p=pList->head;p;p=p->next){ //链表中非常经典的遍历方式
printf("%d\t",p->value);
}
}
链表的删除
1.双向链表,能够获得上一个结点和下一个结点
2.单向链表
Node *q,*p;
for(q=NULL,p=pList->head;p;q=p,p=p->next){
if(number==p->value){
if(q){ //判断边界值,解析见下文
q->next = p->next;
}else{
pList->head = p->next;
}
}
}
如何找寻指针边界呢?
特别注意!通常做法是如果一个指针在->的左边,那么检查一下是否做了p为NULL的判定
Node *q;
for(q=NULL,p=list.head;p;q=p,p=p->next){ //第二个条件;p;做了p不为NULL的判定,所以p=p->next安全
if(p->value==i){ //同样p->value也是安全的
if(q){
q->next = p->next; //q没有任何保证不为NULL,需要区别判断对待
}else{
list.head = p->next;
}
free(p);
}
}
链表的清除
for (p=head;p;p=q){
q=p->next;
free(p);
}
搜索与排序
堆内存和栈内存
程序的内存分配
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。-程序结束后有系统释放
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
堆内存栈内存理论知识
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
注意这里,malloc分配失败会返回空指针,但new分配失败只会抛出异常,需要
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,由编译器决定栈的大小(一般1M/2M),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
申请效率的比较
栈: 由系统自动分配,速度较快。但程序员是无法控制的。
堆: 由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
存取效率的比较
chars[] = "abc"; //栈
char*p3 = "123456"; //123456\0在常量区,p3在栈上。
abc是在运行时刻赋值的;而123456是在编译时就确定的;但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
小心内存泄漏
在堆上分配内存很容易造成内存泄漏,这是C/C++的最大的“克星”,如果你的程序要稳定,那么就不要出现MemoryLeak。所以,我还是要在这里千叮咛万嘱付,在使用malloc系统函数(包括calloc,realloc)时千万要小心。
记得有一个UNIX上的服务应用程序,大约有几百的C文件编译而成,运行测试良好,等使用时,每隔三个月系统就是down一次,搞得许多人焦头烂额,查不出问题所在。只好,每隔两个月人工手动重启系统一次。出现这种问题就是MemeryLeak在做怪了,在C/C++中这种问题总是会发生,所以你一定要小心。
对于malloc和free的操作有以下规则:
1)配对使用,有一个malloc,就应该有一个free。(C++中对应为new和delete)
2) 尽量在同一层上使用,不要malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。
3) malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。
注:虽然现在的操作系统(如:UNIX和Win2k/NT)都有进程内存跟踪机制,也就是如果你有没有释放的内存,操作系统会帮你释放。但操作系统依然不会释放你程序中所有产生了MemoryLeak的内存,所以,最好还是你自己来做这个工作。(有的时候不知不觉就出现MemoryLeak了,而且在几百万行的代码中找无异于海底捞针,Rational有一个工具叫Purify,可能很好的帮你检查程序中的MemoryLeak)
示例
123456789101112131415
//main.cppinta = 0; //全局初始化区char*p1; //全局未初始化区main(){ int b; //栈 char s[] = "abc"; //栈 char *p2; //栈 char *p3 = "123456"; //123456\0在常量区,p3在栈上 static int c =0; //全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char*)malloc(20); //分配得来的10和20字节的区域就在堆区, 但是注意p1、p2本身是在栈中的 strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方}
http://blog.csdn.net/abcjennifer/article/details/39780819
其他
assert
assert(expression) 当expression为假,那么它先想stderr打印一条信息,然后通过调用abort来终止程序运行
注: 需要引入assert.h
gdb
编译生成可执行文件
gcc -g test.c -o output
注:
-g Generate source-level debug information
启动gdb调试output
gdb output
或以tui模式运行gdb
gdb -tui output
运行
run
继续执行
continue
设置断点
b main 表示在main函数入口处加断点
b 10 在第十行加断点
删除断点
d 断点编号
单步运行(跳过函数)
n
单步运行(进入函数)
s
显示信息
info locals 显示所有局部变量
info b 显示所有断点信息
显示变量值
p 变量名
显示变量类型
ptype 变量名
ptype &i
打印数组
p *arr@n n是要打印的元素个数
显示整个栈,即当前存在的所有帧的集合
backtrace
查看栈帧
frame num
注意栈帧编号规则,当前正在执行的帧编号为0,其父帧(即该函数的调用者的栈帧)被编号为1,
调到调用栈的下一个父帧
up
上/下一条命令
ctrl+p/n
参考
http://blog.csdn.net/xiajun07061225/article/details/8960332
常见问题:
1.gdb调试程序,不按顺序执行
因为在linux下程序在编译的时候做了代码优化,如果想要在调试时按顺序执行的话可以通过设置
make CFLAGS="-g -O0"来使得编译器不做代码优化
-O就是优化等级
-O0就是不做优化
常用函数
|
|
参考
FAQ
1.为什么字符串赋值要用strcpy
char arr[10];
*arr = "hello";
printf("%s",arr);
//结果: 直接报错,因为数组除了初始化时可以用char arr[]="",之后只可以对某一位置元素赋值,不可以直接对整体赋值,如果想对整体赋值需要借助strcpy
char arr[10];
char *p=arr;
p[0] = 'h';
printf("%c",arr[0]);
结果: h
这里arr[0]、*p都是左值
这里牵扯到一个概念 左值和右值
左值: 左值可以出现在赋值符号
的两边
右值: 右值是不能对其进行赋值的表达式。右值只能出现在赋值符号右侧。
在赋值符号左边的是左值,不一定是变量
2.->是什么意思?
间接引用运算符
struct T{
int a;
char b;
}s;
struct T *p=&s;
p->a等于s.a等于(*p).a
3.头文件
通常将常量定义 类型定义 函数声明等写入到头文件中
在.c文件中具体实现函数
如Queue.c与Queue.h
.h文件用在执行源文件的开头 .c文件不能被包含
gcc a.c b.c c.c
4.变量运算自动转换
求a和b的平均数
int a = 1;
int b = 2;
double c = (a+b)/2.0;
结果是1.5000,如果2.0换成2 那么(a+b)/2的结果是整数,所以此时c是1.000
5.赋值运算符
在c里赋值(如=)是一个运算符,也就是说是有结果的
a=6的结果是a被赋予的值,也就是6
也就很好理解a=b=6这条语句,它等于a=(b=6)
6.类型占用空间大小跟编译环境和平台有关,这句话怎么理解?
|
|
7.什么是cpu的字长?
当我们在说一台计算机的字长的时候,我们是在说cpu中的寄存器是多宽的,如cpu字长是32位,那么说明一个寄存器的大小是32个比特,同时cpu和ram传递数据时,每一次传递的数据是也是32个比特。这个字长在c语言中就反应为int,即int想要表达的是一个寄存器的大小
8.printf输出char c=-1时的奇怪问题
|
|
为什么会出现这种现象?
因为在将小于int的类型char传递给pritnf的时候,会被转换为int类型,char -1 二进制是11111111被扩展后就是4个字节全是1 最后结果就是4294967295
9.-1的二进制表示是多少?
-1 原码为 10000001
补码为 11111110+1 = 11111111
所以-1二进制为11111111
10.浮点型有效数字的意义
|
|
.30f表示输出小数点后30位
计算机中的数字是离散的 假如数字是在能够表达的两个数字a和b中间,那么会选用较近的一个来表示
11.return -1显示问题
|
|
return返回结果的范围是0~255,与unsigned char范围一致。-1二进制是111111111,按照unsigned char表示就是255
12.&和&
&和&相互抵消
&yptr -> (&yptr) -> (yptr的地址) -> 得到地址上的变量 -> yptr
&yptr -> &(*yptr) -> &(y)-> 得到y的地址,也就是yptr -> yptr
13.为什么数组传入函数后sizeof不对了?
|
|
函数参数表中的数组其实是指针
20是因为是5个4字节整数
8是因为64位系统
14.char*是字符串?
字符串可以表达为char的形式
但char不一定是字符串
char*本意是指向字符的指针,可能指向的是单个字符,也可能是个字符数组
只有它指向的字符数组有结尾’\0’时,才能说它指向的是字符串
同理int* 可能指向单个整型变量,也可能指向整型的数组
15.strcpy判定问题
char s1[] = “abc”;
char s2[] = “abc “;
printf(“%d”,strcmp(s1,s2));
结果: -32
16.restrict关键字
char strcpy(char restrict dst,const char *restrict src);
restrict意思是dst和src不能重叠
17.strchr是寻找第一个字符的位置,那么怎么照第二个字符的位置呢?
|
|
18.大小端
|
|
19.函数未声明情况下使用,不报错
|
|
在c语言中有个传统,就是函数如果没有声明的话,参数默认按照int类型对待
20.指针问题
|
|
21.为什么C语言入口函数是main()?
main()成为C语言入口函数其实和C语言本身无关,你的代码是被一段叫做启动代码的程序所调用的,它需要一个叫做main的地方
操作系统把你的可执行程序装在到内存里,启动执行,然后调用你的main函数
22. 结构体的大小怎么计算?
在TC下:
typedef struct {char ch;int a;}my
//sizeof(int)=2;sizeof(my)=3;
在GCC下(非紧凑模式)
typedef struct { char ch; int a;}my
//sizeof(int)=4;sizeof(my)=8
//在非紧凑模式下,结构体以最大成员类型的长度依次开辟长度,这里最大的是int,也就4个字节,开辟一次后不够,又开辟4个字节,所以是8个字节
在GCC下
typedef struct {char ch;int a;} attribute ((packed)) my
//sizeof(int)=4;sizeof(my)=5
attribute ((packed))作用是告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。这个功能跟操作系统没关系,跟编译器有关
23.C语言优秀的组织方式
通常我们用如下方式组织代码
|
|
说明:
这么写解决了文件重复包含造成的重定义问题;函数、全局变量、结构体变量都能在外部调用;每一处调用变量的文件处不用重复的写声明语句,因为声明语句直接卸载了.h头文件中,直接包含头哦文件即可
http://www.cnblogs.com/goaheadnohesitate/p/3287807.html
24.include详解
include “func.h”相当于将func.h中代码复制过来,引入了相关声明,使得编译可以通过,这时程序并不关心实现在哪里。源文件编译后生成目标文件(.o或.obj文件),目标文件中,这些函数和变量就视作一个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成的.o或.obj文件),这时连接器会去找.o或.obj文件中找到具体实现,再把他们build到makefile中指定的那个可执行文件中。
http://www.cnblogs.com/laojie4321/archive/2012/03/30/2425015.html
25.gcc编译
//将test.c预处理、汇编、编译并连接形成可执行文件,默认输出a.out
gcc test.c
//同上,保存全部临时文件
gcc test.c —save-temps
//-o指定输出文件名
gcc test.c -o test
//-o指定输出文件名
gcc test.c -c test.o
//将test.c预处理输出test.i
gcc -E test.c -o test.i
//将test.i汇编形成test.s
gcc -S test.i
//将汇编输出文件test.s编译输为test.o
gcc -c test.s
//将编译输出文件test.o 连接成最终可执行文件test
gcc test.o -o test
//使用编译优化级别(1-3)编译程序需。级别越大效果越好,但编译时间越长
gcc -O1 test.c -o test
Makefile
使用
直接在所在目录执行 make
示例:
#注释
start:hello.o sum.o test.c
gcc -g hello.o sum.o test.c -o start
hello.o:hello.h hello.c
gcc -g hello.h hello.c -o hello.o
sum.o:sum.h sum.c
gcc -g sum.h sum.c -o sum.o
clean:
rm -f *.o start
变量
定义
变量名=字符串
使用
$(变量名)
追加
变量名:=字符串
示例
C=gcc
HELLO=hello.h hello.c
hello.o:$(HELLO)
$(C) $(HELLO) -o hello.o
伪目标
有的目标文件的存在只是为了形成一条规则,从而让make完成特定的工作,过程中并不产生新的目标文件,这样的目标成为伪目标。例如第一个示例中的clean.常见的还有all
all:exe1 exe2
exe1:exe1.c exe1.h
gcc exe1.c -o exe1
exe2:exe2.c exe2.h
gcc exe2.c -o exe2
clean:
rm -f exe*
条件语句
条件语句用于控制makefile部分,而不能控制shell命令
ifeq else endif
ifeq($(VAR),1)
gcc -o exe1 module
else
gcc -o exe2 module
endif
调试
make的调试很简单,只需要通过-d选项就可以在make执行时打印调试信息
参考:
http://www.cnblogs.com/baochuan/archive/2012/07/11/2583593.html
常见问题
1.Makefile:2: *** missing separator. Stop.
gcc..这行前面必须使用TAB不能使用空格代替,Makefile中每个以TAB开头的命令,make会创建一个SHELL进程去执行它
可以使用cat -e -t -v makefile查看,其中TAB表示为^I
Cmake
cmake . 解析对当前目录下的CMakeLists.txt,生成Makefile文件
http://www.hahack.com/codes/cmake/
26.字符数组的存放字符串的问题
struct Person{
int age;
char name[]; //这里如果使用name[]一定要放在最后一个元素,否则报错 推荐使用char name
};
struct Person p;
char s = “xiaoming”;
p.age = 10;
p.name = (char*)malloc(strlen(s)+1);
strcpy(p.name,s);
printf(“%s”,p.name);
//结果:
报错 error: array type ‘char []’ is not assignable
数组的指针是 char * const 指针常量,所以指针不能改变
修改方式1
struct Person{
int age;
char name[];
};
char s = “hello”;
struct Person p = (struct Person*)malloc(sizeof(struct Person)+strlen(s)+1); //对结构体分配内存
p->age = 10;
strcpy(p->name,s);
printf(“%s”,p.name);
修改方式2
struct Person{
int age;
char *name;
}
….以下同上
27.redis 实现
redis字符串实现 sdshdr
typedef sds *char;
struct sdshdr{
long len; //保存字符串长度(不含\0)
long free; //保存剩余可用空间(已去除\0)
char buf[]; //实际存储的地方
}
redis字符串相关函数中,都是以sds类型数据进行传递沟通的,也就是p->buf,那么我如何知道这个p->buf所处结构体的地址呢?
sds ptr = p->buf - sizeof(struct sdshdr); //ptr就是结构体的指针
注意
使用buf[]存储,sizeof(struct sdshdr)等于8
使用char *buf存储,sizeof(struct sdshdr)不等于8
28.strlen与sizeof的区别
sizeof可以用类型做参数,strlen只能用char*作参数
sizeof返回值的含义:
数组: 编译时分配的数组大小
指针: 存储该指针所用的空间(存储该指针的地址的长度,是长整型,应该是4)
类型: 该类型所占的空间大小
对象: 对象的实际占用空间
函数: 函数的返回类型所占的空间大小。函数的返回类型不能是void.
29.宏的一些困惑
宏中包含#
12345#define ERROR_LOG(module) fprintf(stderr,"error:"#module"\n")解析#是字符串化的意思,表示把后面的参数转换成一个字符串ERROR_LOG(devied=0)转换为fprintf(stderr,"error:devied=0\n")宏中包含##
123456#define TYPE1(type,name) name_##type##_type解析##会把内容进行切分TYPE1(int,c) 转换后是 name_int_type 切分后是name_、type、_type三段,匹配后替换得到name_int_type宏定义中do{}while(0)
12表示一个独立的blockhttp://blog.csdn.net/yasi_xi/article/details/19483197
30.字节对齐问题
什么是字节对齐?
计算机中内存大小的基本单位是字节(byte),理论来讲可以从任意地址访问某种基本数据类型,但实际上,计算机并非是逐字符大小读取内存,而是以2,4或8的倍数的字节快来读取内存,如此一来就会对基本类型的合法地址作出一些限制,即它的地址必须是2,4或8的倍数,那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐
对齐准则是什么?
- 结构体变量的首地址能够被其对齐字节数大小所整除
- 结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足
- 结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足。
为什么要字节对齐?
字节对齐显然浪费了空间,为什么还要这么做呢?最重要偶的考虑是提高内存系统性能。假设计算机总是从内存中读取8个字节,如果一个double字节对齐成8的倍数,那么一个内存操作就可以读写,但如果这个double地址没有对齐,数据就有可能被放在两个8字节中,那么可能需要执行两次内存访问才能完成读写。显然这样是低效的,所以需要字节对齐来提高内存系统性能
跨平台通信
由于不同平台对齐方式可能不同,这样,同样的结构在不同平台其大小可能不同,发送的数据可能出现错乱,因此,为了不同处理器之间能够正确处理消息,我们可以使用两种方式处理。
1.1字节赌气
2.字节对结构进行字节填充
我们可以使用伪指令#pragma pack(n) (n为字节对齐数)来使得结构间一字节对齐。
|
|
31.sizeof
sizeof
- 指针 64位8字节 32位4字节
- 类型 类型长度
- 变量 变量实际长度
数组 数组实际容量
结构体 结构体对齐后的长度
32.void指针
void指针可以指向任意类型的数据,同样的,可以用任意类型的指针对void指针复制
如果要将void指针p复制给其他类型的指针,需要强制类型转换