一些常用、核心编程概念的理解与厘清

小智雅汇 2018-10-14 17:10:13

★ 存储程序概念和内存

内存是电存储,与CPU直接通过地址总线连接,所以速度特别快,且能随机访问。它的缺陷是掉电或程序关闭后,数据会丢失。所以数据持久化需要输出到文件存储到辅助存储器(如硬盘)。

计算机要实现计算的自动化,冯诺依曼提出了“存储程序”的概念!程序描述的数据和处理数据的代码都存储到内存中,由控制器取码和译码,发出控制信号,协调各部件的操作。键盘、磁盘上的文件通过输入存储到内存,内存的数据可以输出到文件或显示器。

内存能随机访问是因为其有内存地址对应,如32位机就是有32条地址总线连接内存与CPU。32位的电脑可以访问4G的内存(2^32=2^10*2^10*2^10*2*2=2^30*4)

★ 编程与分治法

面向过程:函数分解。

面向对象:类与对象分解,在类的内部再分解出多个成员函数。

所以面向对象相对于面向过程而言,其颗粒度要高,就像生产一个产品,面向对象是组件装配,而面向过程是零件装配。

类如同数组、结构体一样,也是一种复合数据类型,复合数据类型有时可以作为一个集合整体操作。复合数据类型的引入就是为了增加编程的颗粒度。

★ 代码重用和标识符重用

函数、类及其重载、模板,标识符的作用域(不同作用域可以使用相同变量)、存续期,以及命名空间的语法定义。

重载是利用其函数的签名不同而产生动态绑定,而模板是一个强类型下的泛型编程。另外,模板也是可以重载的。

★ 函数库和类库

为了将出现频次高的常用代码标准化,引入后直接按接口要求调用即可。就如同用半成品直接组装成品。

★ 强类型的泛型编程

函数模板和类模板实现类型参数化。

★ 使用转义字符的两种方式:

普通字符转义为特殊字符;

特殊字符转义为普通字符;

★ 数据表示

数据表示是指事物的数字化、二进制化。

十进制数字、字符、图像、声音都用二进制表示,二进制数字0、1用晶体管的开、关来表示。

十进制数字可以直接转换为二进制数字。

字符通过使用ASCII或Unicode等编码方案,用一个或多个存储位(Byte)来表示一个字符。

图像的位图是一些像素点的集合,每一个像素点如果用8位来表示,可以得到256种颜色,如果用16位、32位,一个像素点表示的颜色就更多,图像也就更逼真。

声音可以用一定时间间隔取样的振幅来数字化。

★ 浮点数的精确度

如3.45E+16,表示3.45*10^16,浮点的点是指小数点可以浮动,相应改变指数就行。

转换成二进制的形式就是:1.xxxx*2^n,尾数部分就可以表示为xxxx,第一位都是1,可以将小数点前面的1省略,所以23bit的尾数部分,可以表示的精度却变成了24bit。

在内存中的存储形式为:

float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;

double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。

浮点型的数据为什么会精度丢失, 或者说精度不精确呢? 其实是由于我们码农在程序里写的十进制小数,而计算机内部只能用二进制的小数, 所以无法精确的表达。对于二进制小数,小数点右边能表达的值是 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128 … 1/(2^n)。所有这些小数都是一点一点的拼凑出来的一个近似的数值, 所有才会不准确的。

★ 提倡多使用全局常量,少使用全局变量

全局的数据是整个程序可见、也可以共享,全局变量还可以全局更新。全局常量的语法要求是声明的同时进行初始化,只能只读访问,不能通过重新赋值来修改。全局变量的全局更新的特点,是当程序较大时,任何地方都可以修改它,会容易失控。

全局常量:一处定义,多处访问,一处修改,多处修改;

全局变量:一处定义,多处使用,多处修改,影响全局。

(外部变量:一处初始化,多处引用(使用external),多处使用,影响全局。)

★ 变量左值、赋值与初始化

变量的左值用于赋值和初始化。变量出现在表达式右边时是以值的形式出现的,变量出现在左边时是被更新存储的值。变量可以多次更新,但初始化是第一次赋值,没有初始化的变量的值是一个不确定的值。

★ 理解预处理指令

相当于普通文档的查找、替换操作。include是用一个文件去替换,define是用定义的值去替换。

(内联函数你也可以理解为一种查找替换,因为较短的函数产生的调用开支反而不划算。)

★ 常见错误或逻辑错误

1 溢出:基本数据类型都是有值域的,超出值域就会产生溢出。

2 数组越界:如当数组大小是10时,数据下标不能超过9(数据下标从0开始),否则就会产生越界。

3 指针未初始化,使用未初始化的指针去更新一个值时,更改的是一个不确定的内存单元的值,可能会产生意想不到的错误。

long * p;

*p = 123;

假设恰好p的地址值是程序中定义的变量var的地址,你用*p = 123;就会修改var的值,而这不是你的初衷。

4 流的挂起:如一个整型输入的是一个字符。或文件中的一个字符读给了一个整形变量。或打开的文件不存在、路径错误、文件写保护、磁盘已满。

5 内存泄漏:如new的内存单元在使用完后一定需要delete掉,否则会耗费内存资源。

★操作符优先级和结合律

如int (*pf)(int);要按运算符的优先级去理解,()的优先级最高,所以上面*pf加上前面的int表示是一个int指针的声明,后面跟的(int)表示是函数,则int (*pf)(int)是指一个int类型的函数指针。

单目、赋值或复合赋值运算符都是右结合,其它是左结合。

理解++i和i++的区别:

用正常的写法去考虑这种简写。

n=i++ 相当于 n=i; i = i + 1

n=++i 相当于 i = i + 1; n = i;

当没有用于赋值,单独做为一个表达式时,如:

i++;

++i;

两种写法是没有区别的。

运算符优先级与结合律参照:http://c.biancheng.net/view/161.html

★上下文(context)的概念

不管是中文还是西文都有一词多义的情况,需要在上下文的语境中去理解。编程语言的语法虽然不允许有语义的歧义性,但同样有上下文语境的概念。如*既可以是乘法运算符,还可以用于定义指针,又可以解引用指针(引用指针指向的值)。编译器需要在上下文语境中去理解其具体含义:

int i = 5 * 4; //*用作乘法,因为其两个操作数是整型

int *p; //*用作指针声明

p = &i;

*p = 45; //*用作指针的解引用

当*前面有数据类型时,*是指针声明,如果前面没有数据类型,后跟一标识符时,是解引用。

函数的重载,编译器也需要在上下文中(函数签名,也就是函数声明)去理解。类中对象的多态也是如此,往父类方向调用最近同名方法。

上下文语境的存在,是因为毕竟操作符有限,另外,尽量不使用太多的关键字,如static,既可用于定义静态变量(延长变量的存续期),又可用来定义一个变量的作用域(限制全局变量的作用空间)。

★ 程序的输入、输出

程序是对数据的表示、存储和处理,而输入、输出也是其很重要的组成部分(用于交互)。程序的整体是如此(如控制台程序使用输入输出函数,GUI图形用户界面程序使用窗体、菜单和工具栏、事件驱动),程序的组成模块(函数和类)也是哪些,可以用输入、数据处理、输出三个部分去有机构造。

文件的读写相对于内存来说也是一种输入输出。

★ 有了比较运算符为什么还需要逻辑运算符

在分支选择和循环结构中需要使用这两种运算符。而逻辑运算符的非运算可以取反,其它的逻辑运算符可以组合多个条件表达式。

★ C语言的整型与字符型、布尔值、枚举类型变量值

C语言中的字符是用整形数据来表示的(以整数的形式存储在内存,输入输出时进行转换),区别在于使用的存储位不同,二都可以进行转换操作。

C语言中没有布尔类型,用0表示布尔值false,用非0表示布尔值true。

枚举类型定义的变量的取值都是命名整型常量。

★ 循环类型

1 计数控制的循环

一般使用for循环来实现计数控制循环:

for(expression1; expression2; expression3)

statement

在expression中可以使用逗号运算符,可以包含一个或多个计数器变量。expression1一般用于计数器变量的初始化,expression2用作条件判断,expression3一般用做计数器变量的更新,其数值变化的方向与expression2比较的值相一致,以确保能退出循环。)

2 事件控制的循环

2.1 基于哨兵或旗帜;

2.2 基于文件尾;

★ 汇编语言没有控制结构如何实现分支选择与循环?

使用JUMP指令实现跳转,机器语言直接进行地址跳转。

★ 函数的值传递、指针传递、引用传递

int k = 12;

int * p;

p = &k;

int &r = k;

int f1(k);

int f2(k); //int f2(int &k);

int f3(r);

首先,在未调用前,k、p都会在栈内存中分配存储单元。调用后,不管是那一种形式的调用,又会在被调用函数的作用域对应的栈内存中分配存储单元。对于值传递而言,两个空间没有任何联系,所以彼此的更新不会相互影响。而指针传递就不同,两块内存是通过指针连接的,被调函数修改的不是指针存储的地址值,而是其地址值对应的值。对于引用传递而言,形参定义的是一个变量的别名(一个变量两个名字),本身并不分配内存空间,操作的是别名对应的变量。

★ 作用域和生命周期

存储空间节省的考量和标识符重用的考虑以及能够减少模块之间的耦合度。

★ 驱动函数和哑元函数

函数要被调用后才能运行,所以一般用main作为主函数去调用其它函数,main主函数就叫驱动,驱动函数有时也叫客户端。

哑元函数是一个复杂的函数未完整实现前先用一行简单的return语句代替,用于平衡开发进度或与其它函数联调时测试用。

★ break语句和continue语句

当需要在循环体或多重选择(switch)中根据具体的运行情况来确定退出某一次或整个循环的时机,需要使用if语句连同这两个语句一起来变更循环或选择走向。

switch语句中的case分支如果没有break语句,会形成多选的情况。

★ typedef一般用于提高程序的移植性或简化书写

Typedef const double * cdp;

Typedef int bool; //C语言中没有定义bool类型,C语言中是用0表示false,非零表示true。

★ 结构体的集合操作

1 允许的情形:参数传递和函数返回;

2 不允许的情形:输入输出、加减乘除、比较运算。

(类也是一个数据集合,同样不允许一些运算符的集合操作,但可以重载运算符。)

★ 类与对象的this指针

class Time

{

int hrs;

int mins;

int secs;

Set(int hours, int minutes, int secs)

{

hrs = hours; //相当于this->hrs = hours

mins = minutes;

this->secs = secs; //形参与数据成员重名时不能省略this

}

};

★ 三种定义数组的方式

1 定义静态数组:数组的下标只能是常量或常量表达式;

2 定义动态数组

int n;

cin>>n;

int *arr = new int[n];

(arr可以直接当做数组使用,如果是stru *s = new stru[n],stru是一个结构体,则是申请了一个动态结构体数组。)

3 使用STL的vector结构

(C11还可以使用<array>模板类)

★ 数组的集合操作

1 作为函数参数或返回值的引用传递。

2 字符数组的输入、输出

char str[] = {'h','i','/0'};

char str[] = "hi";

cout<<str;

★ 多维数组

在计算机底层是行优先的一维线性的内存存储结构。

二维数组可以理解为一个行列式。

数组名是数组首元素的地址,相当于一个常量指针(数组名是常量,指针名是变量)。如ar[i]和*(ar+i),无论ar是数组名还是指针变量,这两个表达式都是等价的。

(函数名也是函数的地址,可以赋值给一个函数指针,如int (*pf)(int))

★ 定义指针为什么还要定义类型

不管哪个类型的指针,其长度是一样的,还要考虑定义类型的原因主要是:

1 C++是强类型语言,操作符两边类型要一致;

2 解引用时如果没有定义类型,无法确定需要读取多少存储空间;

3 指针偏移(加一个常数)时无法确定其在内存中的偏移量,如double类型的指针在做加1的运算时,实际上是在内存中偏移了8个存储位。

★ 汇编语言没有控制结构如何实现分支选择与循环?

汇编语言用JUMP指令跳转。指令语言直接实现地址跳转。

★ 常量指针

常量指针中并没有常量,只是表示你不能指针去修改或更新指向的数据。

int i =16;

int j = 12;

const int* pi = &i;

//int const* pi = &i;

//*pi = 22; //l-value specifies const object

pi = &j; //允许,指针不是常量,可以更新

i = i+20; //允许,只是不能用指针常量去修改,可以直接通过变量右值来更新左值

cout<<*pi<<endl;

cout<<i<<endl;

指针常量是指你不能使用指针去修改其指向的对象,如*pi = 22;这样做是不允许的。但你可以直接使用变量去更新其值,如上面的i=20;

另外,int const* pi与const int* pi没有区别。

★ C++的结体体和类

两者除了成员变量的访问控制不同以外,其它方面的功能完全一样。

★ 变量的相关概念

作用域(如external)、生命周期(如static)、类型(如int)、名称(作为左值)、引用(别名)、地址(可以取值运算符&获得)、值(作为右值)。

★ 文件的输入输出

文件(file)是在磁盘或固态硬盘上的一段已命名的存储区。内存与磁盘上的文件的交互(读写)如同内存与键盘、显示器的交互(输入输出)。在C语言中,一般通过文件指针进行交互,fopen()函数打开文件后,会返回一个文件指针,使用这个文件指针通过专门的读写函数来对文件内容进行读写。在C++中,还可以定义ifstream、ofstream类的对象,通过对象的方法和操作符来进行文件打开和读写操作。

★ 开关循环(switch)一般使用枚举类型做为标签

枚举类型可以定义有限个变量,这些变量的取值都是命名整型常量。

#include <iostream>

// create named constants for 0 - 6

enum {red, orange, yellow, green, blue, violet, indigo};

int main()

{

using namespace std;

cout << "Enter color code (0-6): ";

int code;

cin >> code;

while (code >= red && code <= indigo)

{

switch (code)

{

case red : cout << "Her lips were red.

"; break;

case orange : cout << "Her hair was orange.

"; break;

case yellow : cout << "Her shoes were yellow.

"; break;

case green : cout << "Her nails were green.

"; break;

case blue : cout << "Her sweatsuit was blue.

"; break;

case violet : cout << "Her eyes were violet.

"; break;

case indigo : cout << "Her mood was indigo.

"; break;

}

cout << "Enter color code (0-6): ";

cin >> code;

}

cout << "Bye

";

return 0;

}

本页共215段,7018个字符,16121 Byte(字节)