编程之基 --- C语言基础大全 III

BACK:编程之基 --- C语言基础大全 II

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以内的素数表
    1. 令x为2
    2. 将2x、3x、4x直至ax<n的数标记为非素数
    3. 令x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕

  • 构造n以内(不含)的素数表
    1. 开辟prime[n],初始化其所有元素为1,prime[x]为1表示x是素数
    2. 令x=2
    3. 如果x是素数,则对于 (i=2; x*i<n; i++) 令 prime[i*x]=0
    4. 令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!

  1. 指针的类型就是指针
    指针大小和内存的编址方式有关,只是恰好与无符号整形大小相同
    他的大小是4字节(32位)就是类似0012ff78(16进制 32位)
    注:如果你的电脑是64位电脑那么他的大小就是8字节!
  2. 指针是用来保存内存地址的
    内存有按32位编制和按64位编制之分
  3. 为什么要给指针定义类型呢?
    只有为指针定义类型
    才能知道指针所指向的变量的大小
    例如: 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

BACK:编程之基 --- C语言基础大全 IV
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值