您的位置:首页 > 编程语言 > C语言/C++

C语言基础知识

2016-06-30 13:03 288 查看
零:声明、定义、初始化

声明:extern 可以置于变量或者函数前面,提示编译器遇到这个变量或者函数的时候,在其他/当前模块里寻找。
extern int a; // b.c 有个全局变量a,那么可以在a.c里声明这个a,然后拿过来用,但是并不代表我重新定义了一个a
extern int function(int a, double b, char c);
// 注意:函数在进行声明的时候,extern 是默认可以省略的。

定义:没有加上 extern 就是定义了。定义后的变量是有存储空间的,可以获取到这个变量的地址,但是不一定有值。
int a;
int arr[10];

初始化:定义变量的同时进行赋值操作。
int a = 10;
int arr[10] = { 0 };

数据类型:
在C语言里的数据类型分为四大种:
1. 基本类型
Linux 64
Windows 64 Linux 32Windows 32
字符: char11
1 1

整型: short22
2 2
int 444
4
long 844
4
long long
8 8 88
size_t
8 8 44

浮点型: float44
4 4
(实型) double88
8 8

2. 构造类型
数组:存储了n个相同基本类型的数据,举例:sizeof(int) * n 个字节大小
char str1[5] = "haha";// 只要用" "括起来的就是字符串,只要是字符串最后一位就是'\0'
char str2[5] = {'h', 'a', 'h', 'a', 'h'};
str1[2] = 'm';
printf("%s\n", str1);

int iarr[5] = {1, 2, 3, 4, 5};
float farr[5] = {1.1, 2.2, 3.14, 4.15, 5.001};

结构体:存储了n个可以不相同基本类型的数据  struct 
联合体/共同体: 存储n个可以不相同基本类型的数据,但是这种类型里的所有数据共享同一块内存空间,内存大小是最大的那个数据类型大小 union
枚举:存储了n个相同基本类型的数据,但是使用的时候只能取其中一个值,内存大小是 sizeof(int);enum

3. 指针类型    32位占4个字节,64位占8个字节
int *p;   // 指针类型变量p就是用来存储地址的,定义指针时候的数据类型,代表这个指针指向的内存空间里存储的值的类型
int a = 10;
p = &a;
*p = 20;
printf("%d\n", a);

char *str = "haha";// char *str 和 char str[5] 都是存储字符串,但是str是一个指针,str是一个数组名
printf("%s\n", str);// char str[5] 存储的"haha"是在栈区, char *str 指向的 "haha" 是在常量区,str存储的是这个字符串在常量区的首地址。

4. 空类型
void 变量 :意思是空类型变量,不接受任何数据
void 函数 :意思是没有返回值
void 指针 :意思是可以接收任何其他类型的指针

一、 字符串函数  <string.h>

1 strlen(char *str); 
这个函数返回值是一个字符串的有效长度(除去'\0'),  有别于 sizeof() 运算符;

2 strnlen(char *str, int maxlen);
这个函数是返回 maxlen 长度以内、不含'\0'的字符串的长度。

3 strcat(char *str1, char *str2);
将参数str2 追加到 str1的后面(覆盖str1后面的'\0')

4 strncat(char *str1, char *str2, int maxlen);
将参数str2 追加到 str1的后面,但是只追加str2的前maxlen个字节长度的字符串。

5 strcmp(char *str1, char *str2);
按字符依次比较两个字符串,直到遇到不同的字符为止:
如果str1大于str2,返回正数(Windows下是返回1,Linux下是返回两个不同字符串的ascii码差值)
如果str1小于str2,返回负数(Windows下是返回-1,Linux下是返回两个不同字符串的ascii码差值)
如果两个字符串相等,返回 0

举例: str1 = "abcde"; str2 = "c";
strcmp(str1, str2);  返回值是(Windows下是返回-1,Linux下是返回两个不同字符串的ascii码差值: -2)

6 strncmp(char *str1, char *str2, int maxlen);
和 strcmp()函数返回值相同,但是只比较前 maxlen 个字符;

7 strcpy(char *str1, char *str2);
将参数 str2 的字符串拷贝到参数 str1 里面。(拷贝的字符包括'\0')

8 strncpy(char *str1, char *str2, int maxlen);
将参数 str2 的前 maxlen 个字符拷贝到 str1 里。

9 strchr(char *str, char ch); //原来是 int ch,但是函数在调用的时候,会转换成char ch
在str中查找指定字符ch,如果找到的话返回ch在str中的位置,如果没找到,返回 NULL // (void*) 0

10 strstr(char *str1, char *str2); 
在str1中查找指定字符串str2,如果找到的话返回str2在str1中的位置,如果没找到,返回 NULL

11 strtok(char *str, char delim);
分解字符串 str 为一组字符串子串,用delim做为字符串的分隔符。
strtok()函数每次分隔会把分隔符的位置置为 '\0', 同时会破坏原先字符串的完整性;
调用函数前的字符串和调用函数后的字符,已经不一样了。所以我们在做字符串分隔的时候,更推荐使用 sscanf();

12 sscanf(char *str, char *format, argument...);
scanf()函数是从键盘上读取用户输入,然后把值写到变量里;
sscanf()函数是从str里读取数据,按照format格式,将数据写入到变量里。
返回值是成功写入的数据数量;

int a, b, c;
char s1[10] = { 0 };
char s2[10] = { 0 };
char s3[10] = { 0 };
sscanf("2016 - 05 - 31", "%d - %d - %d", &a, &b, &c);
// 按照 %d - %d - %d 的格式分隔,把2016、05、31 这三个整数写入到a、b、c里
sscanf("2016 - 05 - 31", "%s - %s - %s", s1, s2, s3);
//  按照 %s - %s - %s 的格式分隔,把2016、05、31 这三个字符串写入到s1、s2、c里

13 sprintf(char *str, char *format, argument...);
printf()函数是把格式化后的结果输出到屏幕上;
sprintf()函数是把格式化后的结果写入到字符串str里;
返回值是 str被写入的字节数,不包括'\0';

char i[10] = "I";
char you[10] = "You";
char str[100] = { 0 };

sprintf(str, "%s love %s", i, you);
// 输出字符串"I love You"到字符串 str 里
sprintf(str, "%10.3f", 0.1234567);
// 输出字符串 "     0.123" 到字符串 str 里(原先的内容会被清空)

 

14 字符串转换函数 <stdlib.h> 
atoi();
把一个 char 类型的数组转换成一个 int.
itoa()把一个 int 类型的数字转换成char类型的字符串(只能在Visual C++ 编译器下使用);
// Linux是没有的
atoll(); 
把一个 char 类型的数组转换成一个 long long.

atof();
把一个 char 类型的数组转换成一个 double.

举例:
char str[10] = ".....";  //int float long long
int / double / long long n = atoi(str) / atof(str) / atoll(str);
printf("%d / %lf / %ld\n", n);

二、函数参数的进栈顺序和运算顺序(引伸出各个平台编译器的不同)

1. 大端对齐和小端对齐:

unsigned int num = 0x12345678;

大端对齐:数值的高位字节存储在内存的低位地址上,数值的低位字节存储在内存的高位地址上。

地址:  0xff1100
0xff1101 0xff11020xff1103

数值: 0x120x340x560x78

小端对齐:数字的高位字节存储在内存的高位地址上,数值的低位字节存储在内存的低位地址上。

地址:  0xff1100
0xff1101 0xff11020xff1103

数值: 0x780x560x340x12

大端:IBM、SUN的服务器CPU都是大端对齐,最早的苹果电脑PowerPC也是大端。
小端:x86\AMD64(美国)架构CPU(复杂指令集)都是小端对齐,ARM(英国)架构CPU(精简指令集)都是小端对齐。
x86 intel
AMD64 AMD  

2. 函数的进栈顺序

#include <stdio.h>

void func(int a, int b, int c) // 三个形参(本质是局部变量),接收实参的值

{
printf("a = %d : %p", a, &a);
printf("b = %d : %p", b, &b);
printf("c = %d : %p", c, &c);

}

int main(void)

{
func(100, 200, 300);  // 三个实参
return 0;

}

// Ubuntu GCC 下编译结果

a = 100 : 0xbf8decb0  +4

b = 200 : 0xbf8decb4  +4

c = 300 : 0xbf8decb8

// Windows Visual C++ 下编译结果

a = 100 : 0x0018F720  +4

b = 200 : 0x0018F724  +4

c = 300 : 0x0018F728

// LLVM Clang 下编译结果

a = 100 : 0x7fff547d59ec -4

b = 200 : 0x7fff547d59e8 -4

c = 300 : 0x7fff547d59e4

C程序在执行的时候,先入栈的数据是在栈底的,栈底是高地址,后入栈的数据在栈顶,栈顶为低地址。

从上面的例子看得出来:

GCC和MSVC下,参数的进栈顺序是"从右往左"。

在LLVM Clang下,参数的进栈顺序是"从左往右"。

3. 函数参数的计算顺序

//1.

#include <stdio.h>

int main(void)

{
int a = 10, b = 20, c = 30;
printf("%d, %d, %d\n", a + b + c, b = b * 2, c = c * 2);
return 0;

}

// Windows Visual C++ 下编译结果

110, 40, 60

// Ubuntu GCC 下编译结果

110, 40, 60

// LLVM Clang 下编译结果

60, 40, 60

//2.

#include <stdio.h>

int a()

{
printf("a\n");
return 1;

}

int b()

{
printf("b\n");
return 2;

}

int main(void)

{
printf("%d, %d\n", a(), b());
return 0;

}

//MSVC 下编译结果

b

a

1, 2

// Ubuntu GCC 下编译结果

b

a

1, 2

// LLVM Clang 下编译结果

a

b

1, 2

4. 函数的默认参数

#include <stdio.h>

void func(int a, int b, int c = 300) // 三个形参(本质是局部变量),接收实参的值

{
printf("a = %d : %p", a, &a);
printf("b = %d : %p", b, &b);
printf("c = %d : %p", c, &c);

}

int main(void)

{
func(100, 200);  // 三个实参
return 0;

}

上面的写法,在LLVM Clang下是可以编译通过的,而且c的值是300,func(100, 200)的值也给了a 和 b。

但是在 MSVC 和 GCC下不允许这么做,也不允许在函数参数列表里赋值。

C编译器:

Microsoft Visual C++ / GNU GCC /LLVM Clang / ICC / Turbo C 

当一个函数的参数列表里有多个参数的时候,C语言没有规定实参的进
1849b
栈顺序和计算顺序,而是由编译器自行决定的。

我们在写代码的时候,尽量不要写出UB(行为未定义、奇葩)语句: 

"Undefined Behavior"简单来说就是:

如果你的程序违反了C标准中某些规则,程序具体执行结果会发生什么,C语言没有定义。

也就是说得到的结果可能是某种奇怪的情况,都是有可能发生的。

比如说,整数溢出就是一个"Undefined Behavior"语句。

"Unspecified Behavior"简单来说就是:

C标准提供了好多种可选方案,但是没有告诉你一定要用哪一种,

比如说,函数参数的计算顺序就是这种情况。

三、一级指针

1. 指针的使用: 32位系统下是 4 个字节,64位系统下是 8 个字节

1) 在定义的时候用 * 号,代表这个变量那个是指针类型
int a = 10;
// 定义一个整型变量,存储整数 10
int *p = &a;
// 定义一个整型指针变量,存储a的地址

2) 在配合表达式使用 * 号,代表取值运算符,可以取出这个地址里的值
printf("%d\n", *p);// 打印p指向的地址里的值
printf("%d\n", *(&a));// 打印a这个地址里的值
printf("%d\n", *p + 1);// 取出值,再加1打印出来

2. 指针的几种特殊定义方式:

1) int * const p;
指针常量:p 是 int*类型,那么const修饰的是p,所以p是常量,表示p指向的地址不可修改,
也就是说,p不能再指向别的地方了,但是可以修改p指向的这个地址里的值。
举例:
int a = 10;
int b = 20;
int * const p = &a;
p = &b;
// 错误
*p = 100;
// 允许

2)  const int *p;
int const *p;
常量指针:p 是 int*类型,那么const修饰的是*p,所以*p是常量,表示p指向的地址里的值不可修改,
也就是说,p里的值不能再重新赋值了,但是可以修改p指向的地址。
int a = 10;
int b = 20;
const int *p = &a;
p = &b;
// 可以
*p = 100;
// 错误

3) const int * const p;
常量指针常量:p 是 int*类型,那么const分别修饰了p 和 *p, 所以p和*p都是常量,表示p指向的地址不可修改,
同时p指向的地址里的值也不可修改。
int a = 10;
int b = 20;
const int *const p = &a;
p = &b;
// 错误
*p = 100; // 错误

《C Primer Plus》 : "自由的代价,是永远的警惕。"

你定义了一个指针,那就一定要知道这个指针指向的什么地方,而且你要保证这个指针是真实有效的,否则我就用程序崩溃来惩罚你。

四、多级指针

#include <stdio.h>

int main(void)

{
int a = 10;
int *p = &a;
// 定义一个一级指针变量,存储了整型变量a的地址
int **pp = &p;// 定义一个二级指针变量,存储了整型一级指针变量p的地址
int ***ppp = &pp;// 定义了一个三级指针变量,存储了整型二级指针变量pp的地址

printf("%p, %p, %p, %p\n", &a, &p, &pp, &ppp);
// 分别打印各个变量自身所在的内存地址

printf("%p, %p, %p, %p\n", &a, p, pp, ppp);
//printf("%d", a);用%d的形式打印a的值:整数
//printf("%p", p);用%p的形式打印p的值:地址
// &a : 打印 变量 a 的地址
// p: 打印变量 a的地址
// pp: 打印变量 p 的地址
// PPP:打印变量 pp 的地址

printf("%p, %p, %p, %p\n", &a, p, *pp, **ppp);
// &a : 打印 变量 a 的地址
// p: 打印变量 a 的地址
// *pp:打印变量 a 的地址
//**PPP: 打印变量 a 的地址

printf("%d, %d, %d, %d\n", a, *p, **pp, ***ppp);
// a: 打印 10
// *P: 打印 10
// **pp:打印 10
// ***ppp: 打印 10

}

五、指针 和 数组的用法

int num[5] = {10, 20, 30, 40, 50};
int *p = num;

打印的值 打印后*p的值是数组里的原值

// 操作地址

*p++ 10
 20 10

*(p++)
*和++的优先级相同,根据结合性(从右往左),那么p先和后自增运算符++结合,
++操作将在表达式完成后进行自增,也就取出p指向的值之后,p指向的下标后移一位(4个字节)。

*++p 20
 20 10

*(++p)
*和++优先级相同,根据结合性(从右往左),那么p先和前自增运算符++结合,
++操作将会立即完成,p指向的下标后移一位(4个字节),然后再取出p指向的值。

// 操作数值

(*p)++ 10
11 11
根据优先级()小括号优先级最高,p先和*相结合,然后再和后自增运算符++结合,
因为是后自增,所以先打印当前下标的值,然后在原值的基础上自增 1,此时原值已被改变

++*p 11
11 11

++(*p)
根据结合性/优先级,*和p先结合,然后再和前自增运算符++结合,
因为是前自增,所以先在原值的基础上自增1,然后在打印这个值,此时原值已被改变。

总结:如果一个表达式里有多个运算符,则先进行优先级比较,先执行优先级高的运算符;
如果优先级相同,那就看结合性,根据结合方向来做运算。

结合性:
从左往右:
小括号()、数组括号[]、成员选择 . 和 ->,双目运算符,逗号运算符

从右往左:
单目运算符、三目运算符、赋值类运算符

/*

++a:  是直接从变量 a 所在的内存地址中取值,并进行加1操作,再执行表达式剩余部分。

a++: 先把变量的值保存在一个临时寄存器里,然后再执行整个表达式,执行完之后,再把a的值自增1,再返回内存里。

CPU -》 寄存器 -》 缓存(L1\L2\L3) -》 来自于内存

CPU只和寄存器做数据交换,对于重复操作的数据会放在缓存里。

但是不管寄存器还是缓存,他们的数据都来自于内存。

*/

六、指针数组 和 数组指针

1. 指针数组:
定义形式: 
int *p
= { 0 };
[]的优先级高于*,那么p先和[]结合,说明这是一个数组。
再和int *结合,说明这个数组里的每个元素都是一个指针,每个元素都能保存一个地址。

1) 使用指针数组保存多个数据的地址

int main(void)

{
int a[3] = { 10, 20, 30};
int *p[3] = { 0 };
for (int i = 0; i < 3; ++i)
p[i] = &a[i];

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

    }

}

2) 使用指针数组保存多个字符串的首地址

#include <stdio.h>

int main(void)

{
char *str[5] = 
{
"ISO/IEC9899:2011",
"Programming",
"Dennis Ritchie",
"c",
"bell ssss"
};

char *str1 = str[1];// 取出 第2行的字符串
char *str2 = *(str + 3); // 取出 第4行的字符串
char ch1 = *(*(str + 4) + 2); //取出 第5行的字符串的第3个字符
char ch2 = (*str + 5)[7];// 取出第1个字符串的第6个字符,并以此做数组首元素,向后遍历到第7个。
char ch3 = *str[0] + 6;  // 取出第1个字符串的第1个字符,然后把这个字符ASCII码加上6,再用%c打印出来

printf("str1 = %s\n",  str1); // Programming
printf("str2 = %s\n",  str2); // c
printf("ch1 = %c\n",  ch1);// l
printf("ch2 = %c\n",  ch2); // 2
printf("ch3 = %c\n",  ch3); // O (不是0)

return 0;

}

2. 数组指针(行指针)
定义形式: int *p;  int (*p)
; int (*p)
[m];
()和[]的优先级相同,但是结合性是从左往右,那么p先和*相结合,说明这是一个指针。
然后再和[]结合,说明这个指针指向了一个数组。

示例:

1)

#include <stdio.h>

void func(int (*p)[3], int n);

int main(void)

{
int a[2][3] = {10, 20, 30, 40, 50, 60};
// 行:   0  1
// 列:   0   1   2   0   1   2 
func(a, sizeof(a));
return 0;

}

void func(int p[2][3], int n);

// 明确行和列的数量

void func(int p[][3], int n);

//明确列的数量

void func(int (*p)[3], int n)  //明确列的数组指针

{
printf("%d\n", p[1][2]);

// 取出第2行的第3个元素:60

printf("%d\n", **p);
//p是行指针,先是取出当前行的列地址,再取出这个列地址里的值: 10

printf("%d\n", (*p + 1)[2]); 
// 先取出列地址,然后往后移动一个int(4个字节),再以这一列的下标为起点,取出后面第2个元素的值:40

printf("%d\n", *(*p + 1));
// 先取出列地址,往后移动1位(1个int),再取出这个下标的值:20

printf("%d\n", *(p[1] + 2));
// 先取出p[1]的列下标地址,再往后移动2位(2个int),再取出这个下标的值:60

printf("%d\n", *(*(p + 1)));
// 先将行指针p后移一位(3个int),再取出这一行的这一列的下标地址,再根据这个下标取值:40

printf("%d\n", *((*p + 1) + 2));
//这个表达式相当于 *(*p + 3),先取出列下标地址,然后往后移动3位(3个int),再取值:40

printf("%d\n", *(*p + 1) + 2);
// 先取出列下标的地址,然后往后移动一位(1个int),再进行取值得出20,再加2,结果是:22

}

// 三维数组同理

void func(int (*p)[3][4])

{
printf("%d\n", *(*(*(p + 0) + 1 ) + 2);  //70

}

int main(void)

{
int a[2][3][4] = 
{
{{10, 20, 30, 40}, {50, 60, 70, 80}, {90, 100, 110, 120}},
{{11, 22, 33, 44}, {55, 66, 77, 88}, {99, 101, 111, 121}} 
};

func(a);
return 0;

}

总结: 
1.指针数组:就是一个数组,这个数组里的每个元素都是一个指针,这个数组在内存空间中占用了n个指针的大小。

2.数组指针:就是一个指针,这个指针指向一个数组,这个指针在内存空间中占用一个指针的大小。

3.二级指针p 和 二维数组名p 的区别:
int **p; 这个是一个整型的二级指针,p是一个可变句柄/钥匙,我们可以让这个指针指向任何我们希望它指向的地方。
这个句柄不需要指定内存空间的大小。
int p
[m]; 这个是一个整型的二维数组,p是引用了这块内存空间的句柄/钥匙,数组在定义的时候,
就被固定指向某个内存空间了,这个空间大小是 sizeof(int) * n * m,而且不可修改p的指向,
p就是一个不可变的常量,永远的都只能指向这里了。

如果想确定某个一维数组的值: * 、 []
如果想确定某个二维数组的值:** 、 [][] 、 *[]

七、内存四区

stack: 栈区,是由编译器自动分配和释放,主要是存放函数参数的值,局部变量的值。

heap:堆区,是由程序员自己申请分配和释放,需要 malloc(); calloc(); realloc();函数来申请,用free()函数来释放
如果不释放,可能出现野指针。

**函数不能返回指向栈区的指针,但是可以返回指向堆区的指针。**

data:数据区 -> 静态(全局)区 和 常量区
静态(全局)区:标有 static 关键字,保存了静态变量和全局变量
1. 初始化的全局变量和初始化的静态变量,在一块区域;
2. 为初始化的全局变量和为初始化的静态变量,在一块区域;
3. 静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。
(如果不初始化,数值数据将被默认初始化为 0, 字符型数据默认初始化为 NULL )。

常量区:这里的数据是只读的,常量和字符串都保存在这里。(不包括字符数组类型的字符串 -> 栈区)
除了第一次初始化外,常量区的数据在程序执行的时候不允许再次赋值。

整个数据区的数组,在程序结束后由系统统一销毁。

code:代码区,用于存放编译后的可执行代码,二进制码,机器码。

static 关键字详解:
static 在C语言里面既可以修饰变量,也可以修饰函数。

static 变量:
1. 静态局部变量:在函数中定义的,生命周期是整个源程序,但是作用域和自动变量没区别。
都是只能在定义这个变量的函数范围内使用,而且只能在第一次进入这个函数时候被初始化,
之后的初始化会跳过,并保留原来的值。退出这个函数后,尽管这个变量还在,但是已经不能使用了。

2. 静态全局变量:全局变量本身就是静态存储的,但是静态全局变量和非静态全局变量又有区别:
1) 全局变量:变量的作用域是整个源程序,其他源文件也可以使用,生命周期整个源程序。
2) 静态全局变量:变量的作用域范围被限制在当前文件内,其他源文件不可使用,生命周期整个源程序。

static 函数(内部函数):
只能被当前文件内的其他函数调用,不能被其他文件内的函数调用,主要是区别非静态函数(外部函数)

总结: 
作用域:变量或函数在运行时候的 有效作用范围 。
生命周期:变量或函数在运行时候的 没被销毁回收 的存活时间。

作用域 生命周期

局部变量   所在代码块内 所在函数结束

全局变量     所有文件内 程序执行结束

静态局部变量   所在代码块内   程序执行结束

静态全局变量   当前文件内 程序执行结束

普通函数   所有文件内 程序执行结束

静态函数   当前文件内 程序执行结束

八、堆区内存

#include <stdlib.h>

1. void* malloc(n * sizeof(int));
请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回 NULL;

2. void* calloc(n, sizoef(int));
请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回NULL ;
(和 malloc() 函数的区别在于,calloc()在创建成功后,会把空间自动初始化为 0 );

3. void *realloc(p, n * sizeof(int));
给一个已经分配了地址的指针 p 重新分配空间,p 是原来空间的首地址,n * sizeof(int) 基于这个首地址重新分配的大小;
1) 如果当前内存段后面有足够的内存空间,那么就直接扩展这段内存,realloc()返回原来的首地址;
2) 如果当前内存段后面没有足够的内存空间,那么系统会重新向内存树申请一段合适的空间,并将原来空间里的数据块释放掉,
而且 realloc() 会返回重新申请的堆空间的首地址;
3) 如果创建失败,返回 NULL, 此时原来的指针依然有效;

4.  void free();
1) free(p); 只是释放了申请的内存,系统将这块内存标记为可用。也就是可以被其他进程使用,但是并不改变 p 的指向;
2) p 所指向的内存空间被释放,所以其他程序就有机会使用这段空间了,相当于 p 指向了不属于自己的空间,里面的数据也是未知的。
(这个就叫野指针)
3) 为了避免野指针,最好在 free(p)之后,将 p = NULL; void *(0);
4) free()函数在执行的时候,其实是把这个块内存返回了内存红黑树上,让别人可以使用这块内存。
从逻辑上来说,释放p之后,你是不能再访问原先p指向的这块内存了,但是现在操作系统没有做到,
所以你还是可以访问到这块内存的,只是里面可能存有的数据不属于你。
free(p)之后,其实系统并没有做数据清空处理,所以你既可以访问这个空间,也可以用里面的值。
但是严格意义上来说,这样做是非法的!会造成野指针!

示例: 

// 如何释放自定义函数内申请的堆空间

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

char *funcA();

char *funcB();

int a = 10; // 全局 初始化区域

char *p1; // 全局 为初始化区域

int main(void)

{
int b;
// 栈区
char arr[] = "hello";// 栈区
char *p2;
// p2 在栈区
const char *p3 = "world!";// p3 在栈区,"world!\0" 在常量区

static int c = 0;// 静态区 初始化区域

char *p;

p1 = (char *)malloc(20);// p1 指向堆区 20个字节

memset(p1, 0, sizeof(char) * 20);// 使用memset()函数将内存空间初始化为 0
strcpy(p1, "Are you Sleep?");// "Are you Sleep?" 是在常量区

printf("%s\n", p1);// p1 指向的 拷贝到堆空间的 "Are you Sleep?" 的首地址,通过首地址打印这个字符串

p2 = funcB();
// p2 接收了funcB()回传堆空间首地址,可以通过这个地址找到funcA()申请的堆空间

free(p2);
// 也可通过 p2 释放 自定义函数里申请的对空劲啊
free(p1);

p1 = NULL;
// 安全起见,释放堆空间指针后, 重置将指针变量置为 NULL
p2 = NULL;

return 0;
// 返回 0 给系统表示 main()正常执行结束,也就代表程序执行结束

}

char *funcA()

{
int a = 10;
const char *pa = "1234567";  // pa 在栈区, "1234567\0" 在常量区
char *pb = NULL;  // pb 在栈区,pb 占4字节(32bit system)

pb = (char *)malloc(20);// pb 指向了一个20个字节大小的堆空间
strcpy(pb, "Yes, I'm!");// 拷贝 字符串给 pb,"Yes, I'm!\0" 在常量区

return pb;
// 返回 指针变量 pb 保存的堆空间首地址给调用函数funcB() 

}

char *funcB()

{
char *pa = NULL;// pa 是一个栈上的指针变量
pa = funcA();
// pa 接收了 funcA()函数返回的堆空间地址
return pa;
// 返回指针变量 pa 保存的堆空间首地址给调用函数 main();

}

论空间分配速度:
栈区确实略快于堆区,
使用栈的时候,是直接从分配的地址里读取值,放到寄存器里,然后再放到目标地址。
使用堆的时候,是先将分配的地址放到寄存器里,然后再从这个地址里取值,再放到寄存器里,再放到目标地址。

论空间访问速度:
栈区和堆区是一样的,都是一个直接寻址的过程,没有区别。

CPU -> 寄存器 > L1 > L2 > L3 (属于缓存) > RAM(内存) > ROM(主板的存储器) >  硬盘

CPU 只和 寄存器做数据存取,寄存器是用来存储临时数据的,对于需要重复操作的数据,会放到缓存里。

不管是寄存器还是缓存,数据都来自于内存, 内存呢又分为四个区....

九、文件操作;

数据I/O流

#include <stdio.h>

1 fopen()函数:打开文件
函数原型:FILE *fopen(char restrict *filename, char restrict *mode);
// restrict C99标准才引进的,属于类型修饰符,表示修饰的这块内存空间只能被这个指针引用和修改,除此之外别无他法。

参数:
filename: 需要打开的文件
mode: 文件打开方式

r 以只读的方式打开文件,前提是这个文件必须存在(只写 r 默认是文本文件)
r+ 以可读可写的方式打开文件,前提是这个文件必须存在(默认是文本文件)。
rb 以只读的方式打开一个二进制文件,前提是这个文件必须存在。
rb+ 以可读可写的方式打开一个二进制文件,前提是这个文件必须存在。

w 以只写的方式打开文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。
w+ 以可读可写的方式打开文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。
wb 以只写的方式打开一个二进制文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。
wb+ 以可读可写的方式打开一个二进制文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。

a 以追加的方式打开只写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。
a+ 以追加的方式打开一个可读可写的文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。
ab 以追加的方式打开一个二进制只写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。
ab+ 以追加的方式打开一个二进制可读可写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。

r(read): 读;
w(write):写;
a(append):追加;
+(plus):读或写,主要是配合r、w、a使用;
t(text):文本文件;
b(binary):二进制文件

返回值:如果文件顺利打开,则返回值是指向这个文件流的文件指针,
如果文件打开失败,返回 NULL (void*)0

一般来说,文件打开失败会做一个文件指针错误判断
FILE *fp = fopen("c:\\code\\text.c", "w+");
if(NULL == fp)
{
//code 
//exit(-1);
}

2 fgetc(); 和 fputc();
1) fgetc()
文件字符读取函数
原型: int fgetc(FILE * stream);
参数: stream: 文件流
返回值: 成功返回获取的字符ASCII码,失败返回 EOF(-1);
举例:
char ch = fgetc(fp);// 从fp指向的文件流里接收文件流里的第一个字符

2) fputc()
文件字符写入函数
原型: int fputc(int ch, FILE * stream);
参数: ch :就是写入的字符,函数在执行的时候,会自动把 ch ASCII码 转换成一个 unsigned char 类型。
stream: 文件流
返回值: 成功返回输出的字符,失败返回 EOF(-1);
举例:
fputc(ch, fp);// 把字符ch写入到 fp 所指向的文件流里。

3 fgets(); 和 fputs();
1) fgets() 读取文件字符串函数
原型:char *fgets(char *str, int size, FILE* fp);
参数: str : 保存从fp指向的文件流里读取的一行字符串。
size: 从文件流里读取的字符串不超过 size 个字符。( 一般会使用size - 1,留一个字符位置给 '\0')
fp : 文件指针  
返回值:成功返回读取的字符串所在的内存首地址,失败返回 EOF(-1);
举例:
char str[20] = { 0 };
fgets(str, 20 - 1, fp);// 从fp指向的文件流的第一行里读取 19个字符,然后放到字符数组 str里。

2) fputs() 写入文件字符串函数
原型:fputs(char *str, FILE* fp);
参数: str: 要写入到文件里字符串
fp: 文件指针
举例:
char str[20] = "Hello Kitty!";
fputs(str, fp);// 向 fp 指向的文件流里写入一个字符串 str,具体怎么写,看 mode 属性。

4 fprintf(); 和 fscanf();
1) fprintf() 将格式化后的数据写入到文件流里
原型: int fprintf(FILE *stream, char *format, argument...);
举例:
int i = 10;
float f = 3.14;
char ch = 'C';
char str[10] = "haha";
fprintf(fp, "%d %f %c %s\n", i, f, ch, str); // 将各个数据按格式写入到文件流里

2) fscanf() 从文件流里获取数据格式化写入输入流里
原型: int fscanf(FILE *stream, char *format, argument... );
举例:
int i;
float f;
char ch;
char str[10];
fscanf(fp, "%d,%f", &i, &f);
// 如果不需要从文件里面写入字符串,那么就可以用逗号或者其他符号来分隔

fscanf(fp, "%s%c", str, &ch);
// 如果文件里需要写入字符串,那么字符串与其他数据之间只能用空格和回车来分隔

5 fread(); 和 fwrite();二进制文件读写函数
函数原型:
size_t fread(void *ptr, size_t size, size_t count, FILE* fp);
size_t fwrite(void *ptr, size_t size, size_t count, FILE* fp);
参数: ptr: 是一个指针,对应 fread()来说,是从文件里读入的数据存放的地址;
对应 fwrite()来说,是写入到文件里的数据存放的地址。
size: 每次要读写的字节数
count : 读写的次数
fp: 文件指针

返回值: 成功读取/写入的字节数

举例:
char str[] = { 0 };
fread(str, sizeof(char) * 10, 1, fp);
// 每次从fp指向的文件中读取10个字节大小,放入字符数组 str 中,总共读1次
fwrite(str, sizeof(char) * 10, 1, fp);
// 每次从str里获取 10个字节大小,写入到 fp 指向的文件中,总共写1次

6 fseek(); 文件指针操作函数
函数原型: size_t fseek(FILE* fp, long offset, int whence);
参数: fp : 文件指针
offset:  偏移量,基于起始点偏移了 offset 个字节 
whence : 起始点(三个):
SEEK_SET 0
文件开头位置
SEEK_CUR 1
当前位置
SEEK_END 2
文件结尾位置

举例
fseek(fp, 0, SEEK_END);
// 将文件指针指向文件结尾,并偏移了 0 个字节,也就是直接将文件指针指向文件结尾
fseek(fp, -10, SEEK_CUR);
// 将文件指针指向当前位置,并偏移了 -10 个字节,也就是将文件指针往前移动10个字节

7 ftell();
文件指针操作函数
函数原型:  long ftell(FILE* fp);
参数: fp  文件指针 
返回值:返回文件指针当前位置,基于文件开头的偏移字节数,

举例: long len = ftell(fp);
// 返回文件指针当前位置,基于文件开头的偏移字节数,保存到 len 里。

8 rewind();
文件指针操作函数
函数原型: void rewind(FILE* stream);
参数: fp 文件指针

举例: rewind(fp);
// 将文件指针重新指向I/O流(文件流)的开头。

stream > istream / ostream -> fstream -> sstream

6\7\8 大例子

FILE *fp = fopen("C:\\code\\a.txt", "r+");
fseek(fp, 0, SEEK_END);// 将文件指针指向文件结尾
long len = ftell(fp);// 获取文件指针位置,得到文件的大小(Byte)
rewind(fp);
// 将文件指针重新指向文件开头

9 fflush();
清空数据流里的数据

函数原型:  void fflush(FILE* stream);
参数: stream 数据流 

举例:
fflush(fp);
// 清空文件流
fflush(stdin);// 清空输入流
fflush(stdout);// 清空输出流

10 int stat(const char *path, struct stat *buf);
// 自行补充

11 rename(); 和 remove();

rename(FILE* filename1, FILE* filename2); 重命名文件
rename("old_name.txt", "new_name.txt");
// 把old_name.txt 重命名为 new_name.txt 

remove(FILE* filename);
remove("C:\\code\\a.txt");
// 将 绝对路径下的 a.txt 文件删除

12 feof();
原型: int feof(FILE* fp);
参数: fp  文件指针 
返回值: 一旦文件指针指向文件结尾,就返回一个真值;否则返回非真值(0)

1. 这个函数达到文件结尾的时候,返回的是一个真值,所以在做判断的时候要注意 !feof(fp)
2. 这个函数必须对文件进行过一次读写操作才会生效,也就是说哪怕这个文件是空的,也必须读写一次,feof()才会返回真值。
文件结束是一个标识符,每次对文件读写都会修改这个标识符的位置,对文件读写一次,文件标识符才会被找到,feof()做出返回操作。

13 fclose();
原型: int flcose(FILE* fp);
参数: fp 文件指针 
返回值: 如果成功释放,返回 0, 否则返回 EOF(-1);

fclose(fp); 表示释放文件指针和相关的文件缓冲区,文件指针不再合法指向那块区域,但是不代表清空对应的区域。

UTF-8 编码格式下 一个汉字 3 个字节

GBK 编码格式下 一个汉字
2 个字节 

// UTF-8 下 汉字逆置原理

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main(void)

{
char str[] = "基尔加丹";
int len = strlen(str);

for (int i = len - 1; i > 0; i -= 3)// UTF-8 编码下是3个字节,GBK下是2个字节
{
printf("%c%c\n", str[i - 1], str[i]);
}

// printf("%c%c\n", str[0], str[1]);// 打印"基"

return 0;

}

不同操作系统的行尾标志:

CR LF \ CR \ LF 

CR 是 '\r' 回车 

LF 是 '\n' 换行

在DOS和NT内核的Windows下,采用的是 回车+换行(CR LF '\r''\n') 来表示下一行的开始

在Unix/Linux下,采用的是 换行(LF '\n') 来表示下一行的开始

在Macintosh下(OS X) ,采用的是 回车(CR '\r') 来表示下一行的开始

十、结构体

1. 结构体的字节对齐:
在C语言里,结构体所占的内存是连续的,但是各个成员之间的地址不一定是连续的。所以就出现了"字节对齐".

结构体变量的大小,一定是其最大的数据类型的大小的整数倍,如果某个数据类型大小不够,就填充字节。
结构体变量的地址,一定和其第一个成员的地址是相同的。

1) 结构体字节对齐 

#include <stdio.h>

#include <string.h>

struct Box

{
int height;
// 高
char a[10];
double width;
// 宽
char type;
// 类型

};

int main(void)

{
struct Box box;
box.type = 'C';// C 类型
strcpy(box.a, "hahaha");
box.height = 4;
// 高度是 4
box.width = 5.5;// 宽度是 5.5

printf("box = %p\n", &box);
printf("box.height = %p\n", &box.height);
printf("box.a = %p\n", box.a);
printf("box.width = %p\n", &box.width);
printf("box.type = %p\n", &box.type);

printf("box = %d\n", sizeof(box));
return 0;

}

2) 初识链表

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

struct Student

{
char *name;
// 姓名
int age;
// 年龄
struct Student *next;
// next 是结构体成员,但是类型是 struct Student * 类型,用来指向某个 struct Student 的结构体变量的。
// 结构体可以看做是一个自定义的数据类型,而且结构体可以嵌套,但是嵌套有条件:
// 结构体只可以嵌套自身类型的结构体指针,但是绝对不能嵌套自身类型的结构体变量
// 比如,不能嵌套 struct Student next; 这种

};

int main(void)

{
struct Student stu, *stup;// 定义了一个结构体变量 stu 和 一个结构体指针变量 stup

stu.name = (char *)malloc(10 * sizeof(char));// 给姓名申请了一个10个字节的堆空间
strcpy(stu.name, "damao");// 拷贝字符串 "damao" 给 stu.name (注意,不能直接赋值,要用拷贝)
stu.age = 18;
// 今年 18岁了

stup = (struct Student *)malloc(1 * sizeof(struct Student));// 给 stup 申请一个堆空间,用来保存两个指针(name,next)和一个int
stup->name = (char *)malloc(10 * sizeof(char));// 给 stup->name 申请一个堆空间,保存字符串
strcpy(stup->name, "ermao");// 拷贝字符串
stup->age = 16;// 今年 16岁了

stu.next = stup;// stu的成员next 指向了 结构体变量 stup 的首地址,链表诞生
stup->next = NULL;// stup的成员 next 指向 NULL, 保证安全。

free(stup->name);// 最后申请的堆 最先释放
free(stup);
// 继续释放
free(stu.name);// 最先申请的堆 最后释放

return 0;
// 程序正常结束

}

End 1 ...(以上知识是老师大猫总结,在此说明)

--------------------------------------------------------------------

一、python基本知识总结

1)  注释:# 或者 ''' xxx '''

2)  变量:相较于C语言少了数组、结构体、联合体、枚举,多了String(字符串)、List(列表)、Tuple(元组)和Dictionary(字典);
列表和元组均可以存储各种数据类型,相当于C语言中的结构体,但列表元素可以修改,而元组不能;

3) 标识符和关键字:标识符区分大小写,查取关键字方法为import keyword->keyword.kwlist;

4) 输入:str=raw_input('提示信息');str接受的任何数据类型均为字符串类型,想要转换int类型,可以使用int(str);
输出:print str 不需要加',',print '输出为%d(%s)'%int(str);每次输出自带换行,想连续输出加',' print str1,str2;

5) 运算符:1.没有'++';'--';2.'/'代表自然除,'//'代表整除;**代表幂;

6) 循环语句:if ;if-else;if -elif-else;while;这些语句均没有括号,但在后面要加上':' 

二、html、CSS

1)  html:
标签分类:双标签(<body>***</body>)、单标签(<mate charset="utf-8" />)
标题标签:<h1>标题一</h1>,<h2>标题二</h2>,<h3>标题三</h3>,<h4>标题四</h4>,<h5>标题五</h5>,<h6>标题六</h6>
段标签 :<p></p>,换行标签<br/>,空格 ,转义字符©©水平分隔符<hr/>;
图片标签:<img src="图片路径" alt="" title="提示信息">,文本链接<a href="网址">文字或者图片</a>、锚链接;
无序标签:<ul></ul>,有序标签<ol></ol>,定义列表<dl></dl>
表格标签:<table></table>,表单标签<form></form>
标签里面可以添加各种属性。color;background-color;font-size;weight;height...

2)  CSS:通过选择器设置标签属性。

二、网络爬虫
网络请求方式:GET和POST,前者是显示提交(输入网址或者要查找的东西能看得到),后者是隐式提交(账号密码的方式)
抓取网页内容步骤:将浏览器设置为代理->启动fiddler进行拦截页面内容->留意是get还是post请求,并将报头信息逐个删除,留下有用的->
选择get或者post对应的方式编写代码。
get:
import urllib2
from lxml import etree
request=urllib2.urlopen("http://www.itcast.cn")
//request.add_header('','')
//request.add_data('')
data=request.read();//data的类型是str
post:
import urllib2
from lxml import etree
output=open('a.txt','w')
request=urllib2.Request("http://www.itcast.cn")
//request.add_header('','')
//request.add_data('')
response=request.read()
output.write(response+'\n')
output.close()
XPath:XPath(XML Path Language)是XML路径语言,它是一种用来定位XML文档中某部分位置的语言。
import urllib2
from lxml import etree
import json
output=open('a.json','w')
request=urllib2.Request("http://www.itcast.cn")
//request.add_header('','')
//request.add_data('')
response=request.read()
html=etree.HTML(response)//html是unicode类型
result=html.xpath('\\a[@class=""]')
for item in result:
dic{}
name1=item.xpath('')[0].text
name2=item.xpath('')[0].text
name3=item.xpath('')[0].text
...
dic{'name1'}=name1
dic{'name2'}=name2
dic{'name3'}=name3
...
line=json.dumps(dic,ensure_ascii=FALSE)+'\n'
output.close(line.encode('utf-8'))
output.close()

End 2 ...

--------------------------------------------------------------------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c语言