流程一览
Eighth Week 数组
1. 数组
定义数组:
-
<类型> 变量名称[元素数量];
-
int grades[100];
-
double weight[20];
-
-
元素数量必须是整数
-
C99之前:元素数量必须是编译时刻确定的字面量
数组:
- 是一种容器(放东西的东西),特点是:
- 其中所有的元素具有相同的数据类型;
- 一旦创建,不能改变大小
- *(数组中的元素在内存中是连续依次排列的)
有效的下标范围:
- 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
- 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
- segmentation fault
- 但是也可能运气好,没造成严重的后果
- 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
-
int a[0]; // 可以存在,但是无用
统计个数示例:
const int number = 10; // 数组的大小,c99
int x;
int count[number]; // 定义数组
int i;
for (i=0; i<number; i++) { // 初始化数组
count[i] = 0;
}
scanf("%d", &x);
while (x!=-1) {
if (x>=0 && x<=9) {
count[x]++; // 数组参与运算
}
scanf("%d", &x);
}
for (i=0; i<number; i++) { // 遍历数组输出
printf("%d:%d\n", i, count[i]);
}
2. 数组运算
集成初始化时的定位:
int a[10] = { // c99 ONLY
[0] = 2, [2] = 3, 6,
};
- 用[n]在初始化数据中给出定位
- 没有定位的数据接在前面的位置后面
- 其他位置的值补零
- 也可以不给出数组大小,让编译器算
- 特别适合初始数据稀疏的数组
数组的大小:
-
sizeof给出整个数组所占据的内容的大小,单位是字节
sizeof(a)/sizeof(a[0])
-
sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
-
这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码
数组的赋值:
int a[] = {2, 4, 6, 7, 1, 3};
int b[] = a;
- 数组变量本身不能被赋值
- 要把一个数组的所有元素交给另一个数组,必须采用遍历
for (i=0; i<length; i++) {
b[i] = a[i];
}
- 数组作为函数参数时,往往必须再用另一个参数来传入数组的大小
- 数组作为函数的参数时:
- 不能在[]中给出数组的大小
- 不能再利用sizeof来计算数组的元素个数!
/*
找出key在数组a中的位置
@param key 要寻找的数字
@param a 要寻找的数组
@param length 数组a的长度
@return 如果找到,返回其在a中的位置;如果找不到则返回-1
*/
int search(int key, int a[], int length);
int main(void) {
int a[] = {2, 4, 6, 7, 1, 3, 5, 9};
int x;
int loc;
printf("请输入一个数字:");
scanf("%d", &x);
loc = search(x, a, sizeof(a)/sizeof(a[0]));
if (loc != -1) {
printf("%d在第%d个位置上\n", x, loc);
} else {
printf("%d不存在\n", x);
}
return 0;
}
int search(int key, int a[], int length) {
int ret = -1;
int i;
for (i=0; i<length; i++) {
if (a[i] == key) {
ret = i;
break;
}
}
return ret;
}
2.1 素数
判断是否能被已知的且<x的素数整除:
int isPrime(int x, int knownPrimes[], int numberOfKnownPrimes) {
int ret = 1;
int i;
for (i=0; i<numberOfKnownPrimes; i++) {
if (x%knownPrimes[i] == 0) {
ret = 0;
break;
}
}
return ret;
}
int main(void) {
const int number = 100;
int prime[number] = {2};
int count = 1;
int i = 3;
while (count < number) {
if (isPrime(i, prime, count)) {
prime[count++] = 1;
}
i++;
}
for (i=0; i<number; i++) {
printf("%d", prime[i]);
if ((i+1)%5) printf("\t");
else printf("\n");
}
return 0;
}
构造素数表:
- 预构造n以内的素数表
- 令x为2
- 将2x、3x、4x直至ax<n的数标记为非素数
- 令x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕
- 构造n以内(不含)的素数表
- 开辟prime[n],初始化其所有元素为1,prime[x]为1表示x是素数
- 令x=2
- 如果x是素数,则对于 (i=2; x*i<n; i++) 令 prime[i*x]=0
- 令x++,如果x<n,重复3,否则结束
#include <stdio.h>
int main() {
const int maxNumber = 25;
int isPrime[maxNumber];
int i;
int x;
for (i=0; i<maxNumberl i++) {
isPrime[i] = 1;
}
for (x=2; x<maxNumber; i++) {
if (isPrime[x]) {
for (i=2; i*x<maxNumber; i++) {
isPrime[i*x] = 0;
}
}
}
for (i=2; i<maxNumber; i++) {
if (isPrime[i]) {
printf("%d\t", i);
}
}
printf("\n");
return 0;
}
2.2 二维数组
二维数组的初始化:
int a[][5] = {
{0, 1, 2, 3, 4},
{2, 3, 4, 5, 6},
};
- 列数是必须给出的,行数可以由编译器来数
- 每行一个{},逗号分隔
- 最后的逗号可以存在,有古老的传统
- 如果省略,表示补零
- 也可以用定位(*C99 ONLY)
Ninth Week 指针
1. 指针
只能变量取地址
- 就是保存地址的变量
int i;
int* p = &i;
int* p, q;
int *p, q; // 与上一行所表达一样,p指针,q是int类型变量
访问那个地址上的变量*
- *是一个单目运算符,用来访问指针的值所表示的地址上的变量
- 可以做右值也可以做左值
-
int k = *p;
-
*p = k+1;
-
*左值之所以叫左值
- 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果;
-
a[0] = 2;
-
*p = 3; // 赋值时,p代表的是地址,\*p代表的是地址所指的值。
-
- 是特殊的值,所以叫做左值
2. 指针应用
指针应用场景一:
- 交换两个变量的值
void swap(int *pa, int *pb) {
int t = *pa;
*pa = *pb;
*pb = t;
}
指针应用场景二a:
- 函数返回多个值,某些值就只能通过指针返回
- 传入的参数实际上是需要保存带回的结果的变量
指针应用场景二b:
- 函数返回运算的状态,结果通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
- -1或0(在文件操作会看到大量的例子)
- 但是当任何数值都是有效的可能结果时,就得分开返回了
- 后续的语言(C++,Java)采用了异常机制来解决这个问题
3. 常见错误
定义了指针变量,还没有指向任何变量,就开始使用指针
4. 数组和指针
传入函数的数组成了什么?
- 函数参数表中的数组实际上是指针
-
sizeof(a) == sizeof(int*)
-
但是可以用数组的运算符[]进行运算
-
int isPrime(int x, int knownPrimes[], int numberOfKnownPrimes) {
int ret = 1;
int i;
for (i=0; i<numberOfKnownPrimes; i++) {
if (x%knownPrimes[i] == 0) {
ret = 0;
break;
}
}
return ret;
}
数组变量是特殊的指针:
-
数组变量本身表达地址,所以
-
int a[10]; int *p=a; // 无需用&取地址
-
但是数组的单元表达的是变量,需要用&取地址
-
a == &a[0]
-
-
[]运算符可以对数组做,也可以对指针做:
- p[0] <==> a[0]
-
*运算符可以对指针做,也可以对数组做:
-
*a = 25;
-
-
数组变量是const的指针,所以不能被赋值
- int a[] <==> int *cosnt a=…
5. 指针和const
指针是const:
- 表示一旦得到了某个变量的地址,不能再指向其他变量(可以改所指地址变量的值,不可以改所指地址)
-
int* const q = &i; // q是const
-
*q = 26; // OK
-
q++; // ERROR
-
所指是const:
- 表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const。变量本身是可以修改的,但是不可以通过那个指针去修改)
-
const int *p = &i;
-
*p = 26; // ERROR!(\*p)是const
-
i = 26; // OK
-
p = &j; // OK
-
转换:
- 总是可以把一个非const的值转换成const的
void f(const int *x); // 表示接受的值,我不会去更改它,你放心!
int a = 15;
f(&a); // ok
const int b = a; // 无论传入f的值是不是const都可以
f(&b); // ok
b = a + 1; // Error
- 当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改
6. 指针运算
1+1=2?
- 给一个指针加1表示要让指针指向下一个变量
int a[10];
int *p = a;
// *(p+1)——>a[1]
- 如果指针不是指向一片连续分配的空间,如数组就是连续分配的空间,则这种运算没有意义
指针计算:
- 这些算术运算可以对指针做:
- 给指针加、减一个整数(+,+=,-,-=)
- 递增递减(++/–)
指针相减是指相隔了多少个数
*p++
- 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
- *的优先级虽然高,但是没有++高
- 常用于数组类的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令
指针比较:
- <,<=,==,>,>=,!=都可以对指针做(地址大小的比较)
- 比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增的
0地址
- 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
- 操作系统会给每个进程一片虚拟的地址空间,都含有虚拟的从0地址开始的连续空间
- 所以你的指针不应该具有0值
- 因此可以用0地址来表示特殊的事情:
- 返回的指针是无效的
- 指针没有被真正初始化(可以先初始化为0,但是并没有意义的值。可以当你使用这个变量的时候系统会崩溃,表明这个值为0或没有初始化)
- NULL是一个预定定义的符号,表示0地址
- 有的编译器不愿意你用0来表示0地址
指针的类型:
- 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
- 但是指向不同类型的指针是不能直接相互赋值的
- 这是为了避免用错指针
指针的类型转换:
- void*表示不知道指向什么东西的指针
- 计算时与char*相同(但不相通)
- 指针也可以转换类型
int *p = &i;
void *q = (void*)p;
- 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量
- 我不再当你是int啦,我认为你就是个void!
- 指针的类型就是指针
指针大小和内存的编址方式有关,只是恰好与无符号整形大小相同
他的大小是4字节(32位)就是类似0012ff78(16进制 32位)
注:如果你的电脑是64位电脑那么他的大小就是8字节!- 指针是用来保存内存地址的
内存有按32位编制和按64位编制之分- 为什么要给指针定义类型呢?
只有为指针定义类型
才能知道指针所指向的变量的大小
例如: int*p;和 double*q;
那么读取*p时 就要从地址p开始读取4字节
读取*q时就要从地址q开始读取8字节
用指针来做什么:
- 需要传入较大的数据时用作参数
- 传入数组后对数组做操作
- 函数返回不止一个结果
- 需要用函数来修改不止一个变量
- 动态申请的内存
7. 动态分配内存
malloc:
#include <stdlib.h>
void* malloc(size_t size);
-
向malloc申请的空间的大小是以字节为单位的
-
返回的结果是void*,需要类型转换为自己需要的类型
-
(int*)malloc(n*sizeof(int))
-
没空间了?
- 如果申请失败则返回0,或者叫做NULL
- 你的系统能给你多大的空间?
#include <stdio.h>
#include <stdlib.h>
int main(void) {
void *p;
int cnt = 0;
while (p=malloc(100*1024*1024)) {
cnt++;
}
printf("分配了%d00MB的空间\n", cnt);
return 0;
}
free():
- 把申请得来的空间还给“系统”
- 申请过的空间,最终都应该要还
- 出来混,迟早都是要还的
- 只能还申请来的空间的首地址
常见问题:
- 申请了每free ——> 长时间运行内存逐渐下降
- 新手:忘了
- 老手:找不到合适的free的时机
- free过了再free
- 地址变过了,直接去free