C程序设计
C语言是一种通用的编程语言,广泛用于系统软件与应用软件的开发。于1969年至1973年间,为了移植与开发UNIX操作系统,由丹尼斯·里奇与肯·汤普逊,以B语言为基础,在贝尔实验室设计、开发出来。C语言具有高效、灵活、功能丰富、表达力强和较高的可移植性等特点,在程序员中备受青睐,成为最近25年使用最为广泛的编程语言。
目前,C语言编译器普遍存在于各种不同的操作系统中,例如Microsoft Windows, Mac OS X, Linux, Unix等。C语言的设计影响了众多后来的编程语言,例如C++、Objective-C、Java、C#等。二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言订定了一套完整的国际标准语法,称为ANSI C,作为C语言的标准。二十世纪八十年代至今的有关程序开发工具,一般都支持匹配ANSI C的语法。
I. C++静态库与动态库
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。所谓静态、动态是指链接。C++静态库与动态库
II. C语言程序四步开发步骤
- 编辑。可以用任何一种编辑软件将在纸上编写好的C语言程序输入计算机,并将C语言源程序文件“.c”以纯文本文件形式保存在计算机的磁盘上(不能设置字体、字号等)。
- 编译。编译过程使用C语言编译程序将编辑好的源程序文件“.c”,翻译成二进制目标代码文件“.obj”。编译程序对源程序逐句检查语法错误发现错误后,不仅会显示错误的位置(行号),还会告知错误类型信息。我们需要再次回到编辑软件修改源程序的错误,然后,再进行编译,直至排除所有语法和语义错误。
- 连接。程序编译后产生的目标文件是可重定位的程序模块,不能直接运行。连接将编译生成的各个目标程序模块和系统或第三方提供的库函数“.lib”连接在一起,生成可以脱离开发环境、直接在操作系统下运行的可执行文件“.exe”。
- 运行程序。如果经过测试,运行可执行文件达到预期设计目的,这个C语言程序的开发工作便到此完成了。如果运行出错,这说明程序处理的逻辑存在问题,需要再次回到编辑环境针对程序出现的逻辑错误进一步检查、修改源程序,重复编辑→编译→连接→运行的过程,直到取得预期结果为止。
III. Code Blocks
Code Blocks作为一款轻量级的C/C++ IDE,它除了能够完成最基本的编辑、编译、调试的功能,还具备以下特点:
开源:每次看到这个词就莫名的激动,特别是对于学生朋友们,囊中羞涩的你再也不用因为使用盗版软件受到内心的谴责。有免费的我们为什么一定要选择花钱呢。
跨平台、跨编译器:Windows、Linux、Mac OS都可以使用,即使将来更换了设备也无需担忧。支持多款编译器,只要简单配置一下就可以轻松切换gcc/g++、Visual C++、Borland C++、Intel C++等20多款编译器。
插件式框架:初学者可能无法理解框架的概念,简单说就是方便添加各种有的没的小功能。
采用C++写成:运行环境非常简单,不用安装其他庞杂的Framework。
升级频繁与维护良好:几乎每个月都有升级包,还有各种热心网友提供功能包。
内嵌可视化GUI设计:IDE的图形界面,采用wxWidgets,如果你听不懂的话只要知道很牛X就可以了。
IV. VC6
IV.I. VC6.0显示行号
VC6.0是一款比较稳定的功能强大的IDE,目前也有很多人在使用。但美中不足的是它不能像其他IDE那样显示行号。这里需要用到一个插件VC6LineNumberAddin,下载地址。
将
VC6LineNumberAddin.dll
文件放在VC6.0安装路径,例如:D:\Program Files\Microsoft Visual Studio\Common\MSDev98\AddIns
;下载的压缩文件中有一个注册表,点击
VC6LineNumberAddin.reg
进行注册,点击后会出现一个对话框,选择“是”就OK了;打开VC6.0,选择【工具】——【定制】,进入“定制”对话框,选择【附加项和宏文件】,勾选
VC6LineNumber
前面的复选框,如果没有该选项,点击【浏览】,选择刚才的VC6LineNumberAddin.dll
添加就可以了,注意,一定要勾选前面的复选框。设置好后点击【关闭】,就会在编辑框左侧显示代码的行号了。
IV.II. VC6.0创建工程
选择菜单File下的New项,会出现一个选择界面,在属性页中选择Projects标签后,会看到近20种的工程类型,我们只需选择其中最简单的一种:“Win32 Console Application”,而后往右上处的“Location”文本框和“Project name”文本框中填入工程相关信息所存放的磁盘位置(目录或文件夹位置)以及工程的名字。
选择菜单Project中子菜单Add To Project下的new项,在出现的对话框的Files标签(选项卡)中,选择“C++ Source File”项,在右中处的File文本框中为将要生成的文件取一个名字,我们取名为Hello(其他遵照系统隐含设置,此时系统将使用Hello.cpp的文件来保存所键入的源程序)。
IV.III. VC6.0打开多个工程实例
xp下设置:打开我的电脑 → 工具 → 文件夹选项 → 在文件类型中选择DSW → 高级 → 编辑 Open 操作 → 去掉“使用 DDE”钩子 →确定完成;
IV.IV. VC6.0 Debug
当你按捺不住激动滴心情点击运行后,发现结果并不是你想要的结果……郁闷了。然后你在代码中加了n条printf来查看 变量的结果……n多循环……运行,再加printf,again and again……终于,要抓狂了……好吧,同学,如果你会用Debug,也许你不用这么纠结。
为了方便程序员排除程序中的逻辑错误,VC 提供了强大的调试功能。每当我们创建一个新的 VC 工程项目时,默认状态就是 Debug(调试)版本。调试版本会执行编译命令_D_DEBUG,将头文件的调试语句 ifdef 分支代码添加到可执行文件中;同时加入的调试信息可以让开发人员观察变量,单步执行程序。由于调试版本包含了大量信息,所以生成的 Debug 版本可执行文件容量会远远大于Release(发行)版本。
在调试程序的过程中,程序员应该记住以下几种技巧1:
- 先调试程序中较小的组成部分,然后调试较大的组成部分;
- 彻底调试好程序的一个组成部分后,再调试下一个组成部分;
- 连续地观察程序流(flow)和数据的变化;
- 始终打开编译程序警告选项 并试图消除所有警告;
- 准确地缩小存在错误的范围。
需要指出的是,主函数不要再用void main()了,这种只有在你学的环境才不会出错,到别的(linux编译器)地方编译是通过不了的。main函数必须要有返回值,如写成int main()在函数结尾时加一个return 0;,这样,所有编译器都不会报错了。
IV.IV.I. 设置断点
在你感觉可能有问题的地方添加断点(按快捷键F9或者点击图中小手按钮),以便运行到断点处好查看运行状态。
设置断点的方法是:将光标停在要被暂停的那一行,选择“Build MiniBar”工具栏按钮“Insert/Remove Breakpoint (F9)”按钮添加断点。如果该行已经设置了断点,那么再次按“F9”功能键会清除该断点。
设置断点后,可以按“F5”功能键启动 Debug模式,程序会在断点处停止。我们可以接着单步执行程序,观察各变量的值如何变化,确认程序是否按照设想的方式运行。
IV.IV.II. 调试命令
我们也可以在 VC“Build”(组建)菜单下的“Start Debug”(开始调试)中点击 Go(F5)命令进入调试状态,Build 菜单自动变成 Debug 菜单,提供以下专用的调试命令:
- Go(F5) 从当前语句开始运行程序,直到程序结束或断点处。
- Step Into(F11) 单步执行下条语句,并跟踪遇到的函数。
- Step Over(F10) 单步执行(跳过所调用的函数)
- Run to Cursor(Ctrl+F10) 运行程序到光标所在的代码行。
- Step out(Shift+F11) 执行函数调用外的语句,并终止在函数调用语句处。
- Stop Debugging(Shift+F5) 停止调试,返回正常的编辑状态
必须在运行程序时用 Go 命令(而不是 Execute)才能启动调试模式。在调试模式下,程序停止在某条语句,该条语句左边就会出现一个黄色的小箭头。我们随时中断程序、单步执行、查看变量、检查调用情况。比如,按“F5”功能键进入调试模式,程序运行到断点处暂停;不断按“F10”功能键,接着一行一行地执行程序,直到程序运行结束。
IV.IV.III. 查看变量
单步调试程序的过程中,我们可以在下方的Variables (变量)子窗口和Watch(监视) 子窗口中动态地察看变量的值。Variables 子窗口中自动显示当前运行上下文中的各个变量的值变量,而 Watch 子窗口内只显示在此 Watch 子窗口输入的变量或表达式的值。随着程序的逐步运行,也可以直接用鼠标指向程序中变量查看其值。Variables 子窗口中,我们可以清楚地看到,程序已经为自动型变量 first、second、big 分配了内存,但它们的初始值是随机的。
Variables 子窗口有 3 个选项卡:Auto、Locals 和 This。
- Auto 选项卡: 显示出当前语句和上一条语句使用的变量,它还显示使用 Step over 或 Step out 命 令后函数的返回值。
- Locals 选项卡:显示出当前函数使用的局部变量。
- This 选项卡: 显示出由 This 所指向的对象(C 语言不用 this)。
如果变量较多,自动显示的Variables 窗口难以查看时,还可以在右边的Watch 子窗口中添加想要监控的变量名。例如,在 Watch1 子窗口中添加了变量“first”。我们还可以直接将变量拖动到 Watch 子窗口的空白 Name 框中。 添加结束后,该变量的值会被显示出来。并且随着单步调试的进行,会看到变量 first 的值逐渐变化。如果各变量的值按照设想的方式逐渐变化,程序运行结果无误,本次开发就顺利结束了。如果发现各变量值的变化和设想的不一致,说明程序存在逻辑错误,那就需要停止调试,返回编辑窗口,查错并修改程序。
IV.IV.IV. 查看内存
数组和指针指向了一段连续的内存中的若干个数据。可以使用 memory 功能显示数组和指针指向的连续内存中的内容。在 Debug 工具条上点 memory 按钮,弹出一个对话框,在其中输入数组或指针的地址,就可以显示该地址指向的内存的内容。
IV.V. 常见的错误
IV.V.I. 等于运算符的误用
编译时检查有助于发现等于运算符的误用。请看下述程序段: 1
2
3
4
5
6
7void foo(int a,int b)
{
if ( a = b )
{
/ * some code here * /
}
}
这种类型的错误一般很难发现!程序并没有比较两个变量,而是把b的值赋给了a,并且在b不为零的条件下执行if体。一般来说,这并不是程序员所希望的(尽管有可能)。这样一来,不仅有关的程序段将被执行错误的次数,并且在以后用到变量a时其值也是错误的。
IV.V.II. 未初始化的变量
编译时检查有助于发现未初始化的变量。请看下面的函数: 1
2
3
4
5
6
7
8
9
10void average ( float ar[], int size )
{
float total;
int a;
for( a = 0;a<size; ++a)
{
total+=ar[a];
}
printf(" %f\n", total / (float) size );
}
IV.V.III. 变量的隐式类型转换
在有些情况下,C语言会自动将一种类型的变量转换为另一种类型。这可能是一件好事(程序员不用再做这项工作),但是也可能会产生意想不到的效果。把指针类型隐式转换成整型恐怕是最糟糕的隐式类型转换。 1
2
3
4
5
6
7
8
9void sort( int ar[],int size )
{
/* code to sort goes here * /
}
int main()
{
int arrgy[10];
sort( 10, array );
}
IV.V.IV. 运算结果出现 1.#IND, 1.#INF nan, inf
进行浮点数编程时,如果没有注意,常常会出现输出类似 1.#IND, 1.#INF 或者 nan, inf 之类奇怪的输出。这通常隐含了浮点数操作的异常。
1.#INF / inf:这个值表示“无穷大 (infinity 的缩写)”,即超出了计算机可以表示的浮点数的最大范围(或者说超过了 double 类型的最大值)。例如,当用 0 除一个整数时便会得到一个1.#INF / inf值;相应的,如果用 0 除一个负整数也会得到 -1.#INF / -inf 值。
-1.#IND / nan:这个的情况更复杂,一般来说,它们来自于任何未定义结果(非法)的浮点数运算。"IND"是 indeterminate 的缩写,而"nan"是 not a number 的缩写。产生这个值的常见例子有:对负数开平方,对负数取对数,0.0/0.0,0.0*∞, ∞/∞ 等。举个例子,如果log()内的值是1.#INF,得到的log值就会是,1.#INF。
所以简而言之,如果遇到 1.#INF / inf,就检查是否发生了运算结果溢出除零,而遇到 1.#IND / nan,就检查是否发生了非法的运算。很多 C 库都提供了一组函数用来判断一个浮点数是否是无穷大或 NaN。int _isnan(double x)
函数用来判断一个浮点数是否是 NaN,而 int _finite(double x)
用以判断一个浮点数是否是无穷大2。
IV.V.V. 出现stack overflow问题
一般遇到这个问题,有两个常见的情况,一个是存在函数的递归调用,另一个是函数中定义了一个较大的数组或者别的变量。
在函数的递归调用中,函数中定义的局部变量所占的空间要直到递归结束才能被释放,这样函数不停的递归,堆栈早晚会被用完,解决这一问题的办法是在递归函数中每次动态分配变量的内存,在使用结束的时候释放内存。遇到这种情况更改堆栈的最大空间大小是没有用的,要从代码的优化入手。
堆栈的大小只有1M,如果在函数中定义了一个占用内存比较大的变量,那么也会导致堆栈溢出。这种情况只需在定义的时候定义为静态变量就行了,因为静态变量是不占用堆栈内存的。
还可以通过修改堆栈的最大空间来解决问题,把project设置里的堆栈加大就可以了,默认是1M,你可以加大到10M试试. 具体如下:project-> setting-> link: 在category里选择output,在stack的Reserve里输入0x10000000试试。对于遇到这样的问题建议从代码方面去解决,不要盲目的依靠修改堆栈空间来解决,毕竟有的问题靠修改空间是解决不了的,如递归中产生的stack overflow。
V. VS-XP
VS编译出来的程序如何在XP-SP2以下的系统运行。
1 | // Including SDKDDKVer.h defines the highest available Windows platform. |
安装VS2012 Update1包,更改项目属性:
(1)配置属性--常规--平台工具集:Visual Studio 2012 - Windows XP (v110_xp)。
(2)链接器--系统--子系统:控制台或窗口,所需的最低版本5.01
(3)C/C++--代码生成--运行库:多线程静态链接库(Release(LIBCMT.lib)):/MT,Debug(LIBCMTD.lib):/MTD)
也可在stdafx.h文件中添加如下代码:
1 | // 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 |
如若进行上述设置后,没有效果,则检查程序引用的外部库属性,是否也进行了相应设置。
VI. C语言
一般来说,按结构化程序设计原则编写的程序是易于调试和修改的,下面将介绍其中的一些原则3:
- 程序中应有足够的注释;
- 函数应当简洁;
- 程序流应该清晰,避免使用goto语句和其它跳转语句;
- 函数名和变量名应具有描述性;
VI.I. 头文件预编译
所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头文件(如Windows.H
、Afxwin.H
)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。
预编译头文件通过编译stdafx.cpp
生成,以工程名命名,由于预编译的头文件的后缀是“pch”,所以编译结果文件是projectname.pch
。
编译器通过一个头文件stdafx.h来使用预编译头文件。stdafx.h
这个头文件名是可以在project的编译设置里指定的。编译器认为,所有在指令#include "stdafx.h"
前的代码都是预编译的,它跳过#include "stdafx. h"
指令,使用projectname.pch
编译这条指令之后的所有代码。
因此,所有的CPP实现文件第一条语句都是:#include "stdafx.h"
。
VI.II. 主函数
main
是C/C++的标准入口函数名。WinMain
是windows API窗体程序的入口函数。(int WINAPI WinMain()) 中 WINAPI是__stdcall宏,在windef.h
中定义的。_tmain
_tWinMain
是Unicode版本函数别名,对应与wmain和wWinMain。工程中最好用这类函数。_tmain
的定义在<tchar.h>
可以找到,如#define _tmain main
,所以要加#include <tchar.h>
才能用。_tmain
是个宏,如果是UNICODE则他是wmain()否则他是main()。
VI.III. 函数声明/定义/调用
- 如果函数没有声明,应该在调用前定义;
- 可以在函数头声明;
- 一般应在文件头声明,函数原型的声明在实际运用中, 会集中声明在头文件(
*.h
)里面。 - 函数可互调用, 但不能嵌套;
- 如果函数没有参数, 最好是 fun(void), 不过 fun() 也行; 如果函数没有返回值, 要注明返回类型是 void; 在 C 语言中调用无参函数也要带括号.
- 声明函数时可以省略形参;
- 函数的参数一般不要超过 7 个;
VI.IV. 类型定义
VI.IV.I. void
- void 修饰变量
void 几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void 变量。void 不能代表一个真实的变量。void 体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人(人妖不算)。
void 真正发挥的作用在于:对函数返回的限定;对函数参数的限定。
任何类型的指针都可以直接赋值给void *
,无需进行强制类型转换。但这并不意味着,void *
也可以无需强制类型转换地赋给其它类型的指针。因为“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”。
- void 修饰函数返回值和参数
如果函数没有返回值,那么应声明为void 类型。在C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。为了避免混乱,我们在编写C 程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为void 类型。
如果函数无参数,那么应声明其参数为void。所以,无论在C 还是C++中,若函数不接受任何参数,一定要指明参数为void。
- void 指针
千万小心又小心使用void 指针类型。按照ANSI(American National Standards Institute)标准,不能对void 指针进行算法操作。
如果函数的参数可以是任意类型指针,那么应声明其参数为void *
。
VI.IV.II. const
const在C语言中算是一个比较新的描述符,我们称之为常量修饰符,即就是说其所修饰的对象为常量4。当你代码中想要设法阻止一个变量被改变,那么这个时候可以选择使用const关键字。在你给一个变量加上const修饰符的同时,通常需要对它进行初始化,在之后的程序中就不能再去改变它。
因为预处理语句虽然可以很方便的进行值得替代,但它有个比较致命的缺点,即预处理语句仅仅只是简单值替代,缺乏类型的检测机制。这样预处理语句就不能享受C编译器严格类型检查的好处,正是由于这样,使得它的使用存在着一系列的隐患和局限性。
VI.V. 变量
在《C语言函数的参数和返回值》中提到,形参变量要等到函数被调用时才分配内存,调用结束后立即释放内存。这说明形参变量的作用域非常有限,只能在函数内部使用,离开该函数就无效了。所谓作用域(Scope),就是变量的有效范围。不仅对于形参变量,C语言中所有的变量都有自己的作用域。决定变量作用域的是变量的定义位置。
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。
VI.V.I. C++/作用域
C++变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为6种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
1>全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
2>静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
3>局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
4>静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
从分配内存空间看:
1>全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间
2>全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
1)静态变量会被放在程序的静态数据存储区(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。应予以注意。
A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
C.设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题,因为他们都放在静态数据存储区,全局可见;
D.如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为:带“内部存储器”功能的的函数)
E.函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。
VI.VI. 动态内存分配
到目前为止,我们的程序中我们只用了声明变量、数组和其他对象(objects)所必需的内存空间,这些内存空间的大小都在程序执行之前就已经确定了。但如果我们需要内存大小为一个变量,其数值只有在程序运行时 (runtime)才能确定,例如有些情况下我们需要根据用户输入来决定必需的内存空间,那么我们该怎么办呢?答案是动态内存分配(dynamic memory)。
- 操作符new的存在是为了要求动态内存。new 后面跟一个数据类型,并跟一对可选的方括号[ ]里面为要求的元素数。它返回一个指向内存块开始位置的指针。我们建议在使用new之后总是检查返回的指针是否为空(null)。
- 既然动态分配的内存只是在程序运行的某一具体阶段才有用,那么一旦它不再被需要时就应该被释放,以便给后面的内存申请使用。操作符delete 因此而产生。
void *malloc(long NumBytes)
:该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。申请了内存空间后,必须检查是否分配成功。malloc()从堆里面获得空间,也就是说函数返回的指针是指向堆里面的一块内存。void free(void *FirstByte)
: 该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。free()释放的是指针指向的内存!注意!释放的是内存,不是指针!指针并没有被释放,指针仍然指向原来的存储空间。指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容的垃圾,是未定义的,所以说是垃圾。因此,释放内存后把指针指向NULL,防止指针在后面不小心又被引用了。
VI.VII. 运算符
VI.VII.I. 除法求余
除法运算符“/”。二元运算符,具有左结合性。参与运算的量均为整型时,结果为整型,舍去小数。如果运算量中有一个为实型,结果为双精度实型。除号的正负取舍和一般的算数一样,符号相同为正,相异为负。
求余运算符“%”,二元运算符,具有左结合性。参与运算的量均为整型。求余运算的结果等于两个数相除后的余数(a%b=a-(int)(a/b)\*b)
。5%2.0
和5.0%2
的结果是语法错误。求余符号的正负取舍和被除数符号相同。
VI.VII.II. 次方
pow() 函数用来求 x 的 y 次幂(次方),其原型为:double pow(double x, double y)
,然后将结果返回。设返回值为 ret,则 ret = \(x^y\)。
可能导致错误的情况:
- 如果底数 x 为负数并且指数 y 不是整数,将会导致 domain error 错误。
- 如果底数 x 和指数 y 都是 0,可能会导致 domain error 错误,也可能没有;这跟库的实现有关。
- 如果底数 x 是 0,指数 y 是负数,可能会导致 domain error 或 pole error 错误,也可能没有;这跟库的实现有关。如果发生 domain error 错误,那么全局变量 errno 将被设置为 EDOM;如果发生 pole error 或 range error 错误,那么全局变量 errno 将被设置为 ERANGE。
- 如果返回值 ret 太大或者太小,将会导致 range error 错误。
1 | #include <stdio.h> |
VI.VIII. 类型转换
VI.IX. 指针
1 | TYPE *name 定义某个类型的指针,取地址。 |
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示。
1 | int a =100; |
VI.X. 数组与字符串长度
数组与字符串长度sizeof()、strlen()、string的length()和size()
sizeof()不需要头文件,和int一样编译器会识别。sizeof()返回的是变量声明后所占的内存数,不是实际长度,此外sizeof不是函数,仅仅是一个操作符,strlen是函数,在string.h中。(VC中的sizeof的用法总结)
c++中的字符串string的长度,size()和length()没有区别。为了兼容等,这两个函数一样。length是因为沿用C语言的习惯而保留下来的,string类最初只有length,引入STL之后,为了兼容又加入了size,它是作为STL容器的属性存在的,便于符合STL的接口规则,以便用于STL的算法。 string类的size()/length()方法返回的是字节数,不管是否有汉字。
1 | string str1=”xxxxx”; |
VI.X.I. sizeof()
求所占的字节数
- 对于整型字符型数组
1 | int A[]={1,4,5,2,8,6,0}; |
- 对于整型或字符型指针
1 | int *p; |
VI.X.II. strlen()
字符数组或字符串所占的字节数,就是指实际字符串或字符数组的实际长度(不是所占空间的字节数)。strlen所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符\0
为止,然后返回计数器值。
- 字符数组
1 | char A[6]={'a','b','\0','d','e','r'}; |
- 字符指针
1 | char C[]={"abcdef"}; |
- 其他结构
1 | class X |
- 有关空类
1 | #include <iostream> |
VI.XI. memset函数
【函数说明】memset() 会将 ptr 所指的内存区域的前 num 个字节的值都设置为 value,然后返回指向 ptr 的指针。其原型为:
头文件:#include <string.h>
void * memset( void * ptr, int value, size_t num );
参数说明:
- ptr 为要操作的内存的指针。
- value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int 和 char 可以根据 ASCII 码相互转换。注意:参数 value 虽声明为 int,但必须是 unsigned char,所以范围在0 到255 之间。
- num 为 ptr 的前 num 个字节,size_t 就是unsigned int。
memset() 可以将一段内存空间全部设置为特定的值,所以经常用来初始化字符数组。例如:
char str[20];
memset(str, '\0', sizeof(str)-1);
范例:
1 | #include <stdio.h> |
执行结果: -------c.biancheng.net
注意:字符数组是可以被修改的,字符串是只读的,不能被修改,而 memset() 又必须修改 str,所以不能将
char str[] = "http://c.biancheng.net"
; 声明为char *str = "http://c.biancheng.net"
;,否则运行时会报错。
VI.XII. FOR循环
在VC6中, for(int j = 0; j < 16; ++j)
中j的作用域在for所在的域中。 而标准C++中, j的作用域在for语句块内。
VI.XIII. 数组与矩阵
C语言支持一维数组和多维数组。如果一个数组的所有元素都不是数组,那么该数组称为一维数组。一维数组的定义和引用
在进行科学计算的时候,矩阵是一种很常见的数据类型。但是作为基本算法实现和工程应用的时候C语言并没有提供该数据类型,而是利用二维数组定义该数据类型。
二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。
以下面的二维数组 a 为例:
1 | int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; |
C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。
C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。
VI.XIII.I. 矩阵的定义和初始化
但是如果矩阵在程序中作为程序返回值的话就不能简单利用二维数组类实现了。以下分享几种C语言矩阵的定义和初始化。
VI.XIII.I.I. 动态申请矩阵存储空间
1)能够动态申请和释放存储空间; 2)对于将矩阵作为返回值的情况具有独特的优势。
1 | //定义结构体类型,这里需要利用指针和结构体,其中m和n分别表示矩阵的行和列 |
VI.XIII.I.II. 静态申请矩阵存储空间
静态存储实现,该方法的好处是简单以实现;相对于第一种方法的缺点是: 1)静态实现,不能动态的申请和释放空间,对电脑内存要求高; 2)对于将矩阵作为函数返回值的情况,不能使用该方法。
1 | double matrix[10][15]; |
VI.XIII.II. 矩阵输入
1 | #include<stdio.h> |
VI.XIII.III. 矩阵生成
1 | int a[5][5]={0};//先把5阶矩阵中所有元素值附0 |
1 | int i,j; |
1 | int i,j,a[5][5]={0};//全赋值为0; |
- 函数型
1 | void getIdentityMatrix(int n, int** array) |
1 | void getIdentityMatrix(int n, double *array) |
VI.XIII.IV. 矩阵转数组
设新的一维向量为b[],其长度应该是a的行维的长度(设为N),和列维长度M的乘积,即b共有MN个元素,对于每一个下标k,有k = iM + j,
1 | for(int i = 0; i<N ;i++) |
VI.XIII.V. 数组赋值
把一维数组赋给另外一个一维数组:
- 逐个赋值法。
1
2
3
4
5
6int a[10], b[10];
int i;
for(i = 0; i < 10; i ++)
{
b[i] = a[i];
} 整体复制法。
1
2
3
4int a[10], b[10];
% memcpy声明与string.h中
% 形式为 void * memcpy(void *dst, void *src, int length);
memcpy(b,a,sizeof(a));
VI.XIII.VI. 数组与数组指针
区别对待数组指针5:arr 本身就是一个指针(这种表述并不准确,严格来说应该是“arr 被转换成了一个指针”。),可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以int *p = arr
;也可以写作int *p = &arr[0]
;。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。
数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
1
2
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;
VI.XIII.VII. 数组作为函数参数
数组用作函数参数有两种形式6,一种是把数组元素(下标变量)作为实参使用;另一种是把数组名作为函数的形参和实参使用。
用数组名作函数参数时,则要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。当形参和实参二者不一致时,即会发生错误。
在数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组。实际上是形参数组和实参数组为同一数组,共同拥有一段内存空间。
VI.XIV. 类与结构体
类是从C语言中的结构体演变而来,结构体的成员变量也就演化成类的成员变量,这时类只能存放数据。为了在类内部操纵这些数据,增加了成员函数的功能。所谓成员函数就是在类的内部定义,用来操作类的成员变量的函数。随后对成员变量和成员函数附上“私有”、“保护”和“公共”的访问权限,于是“类”便大致成型。事实上,C++中结构体的功能大致和类相当,也有了成员函数。“成员”是成员变量和成员函数的统称7。
VI.XV. 矩阵求逆
1 | #include<stdio.h> |
VI.XVI. 生成随机数
浅谈C语言中如何取随机数
用C语言的rand()和srand()产生伪随机数的方法总结
C语言:随机函数rand()、srand()、random()和randomized()的区别和用法
C语言产生满足正态分布的随机数
1 | #include |
VI.XVII. JACOBIAN METHOD
Code for JACOBIAN METHOD in C Programming
computes all eigenvalues and eigenvectors
parallel implementation of Jacobi's method for solving the linear system
NUMERICAL SOLUTION OF A STIFF (OR DIFFERENTIAL ALGEBRAIC) SYSTEM OF FIRST 0RDER ORDINARY DIFFERENTIAL EQUATIONS
1 | n = no of equations |
VI.XVIII. Cholesky分解
Cholesky分解法又称三角分解法,或称因子化法。
设线性方程组\(AX=b\)(1),式中A为对称、正定的矩阵。对于对称、正定的矩阵A,可进行分解 \(A=LD{L^T}\)(2),式中L是下单位三角阵,D是对角线矩阵。 右端项列向量(列阵)也作相应的分解\(b=LD{b'}\)(3)。将式(2)和式(3)代入方程(1),得到上三角方程组\({L^T}x={b'}\)。再按诸如高斯消元法的回代过程就可解出\(x\)。8
1 | #include<stdio.h> |
1 | #include <stdio.h> |
1 | /* file: choesky.c */ |
VI.XIX. 矩阵的逆
A是可逆矩阵的充分必要条件是,即可逆矩阵就是非奇异矩阵。
伪逆矩阵是逆矩阵的广义形式。由于奇异矩阵或非方阵的矩阵不存在逆矩阵,但在matlab里可以用函数pinv(A)求其伪逆矩阵。基本语法为X=pinv(A),X=pinv(A,tol),其中tol为误差,pinv为pseudo-inverse的缩写:max(size(A))*norm(A)*eps
。函数返回一个与A的转置矩阵A' 同型的矩阵X,并且满足:AXA=A,XAX=X.此时,称矩阵X为矩阵A的伪逆,也称为广义逆矩阵。pinv(A)具有inv(A)的部分特性,但不与inv(A)完全等同。 如果A为非奇异方阵,pinv(A)=inv(A),但却会耗费大量的计算时间,相比较而言,inv(A)花费更少的时间。
广义逆阵(generalized inverse)也称为伪逆矩阵(pseudoinverse),是在数学矩阵领域内的名词,一矩阵A的广义逆阵是指另一矩阵具有部分逆矩阵的特性,但是不一定具有逆矩阵的所有特性。假设一矩阵A∈Rnm及另一矩阵Ag∈mn,若Ag满足此条件,AAgA=A,则Ag即为A的逆矩阵。构建广义逆阵的目的是针对可逆矩阵以外的矩阵(例如非方阵的矩阵)可以找到一矩阵有一些类似逆矩阵的特性。任意的矩阵都存在广义逆阵,若一矩阵存在逆矩阵,逆矩阵即为其唯一的广义逆阵。有些广义逆阵可以定义在和结合律乘法有关的数学结构中。可以借助SVD(奇异值分解)来求解伪逆。
假定拟计算一般矩阵A的Moore-Penrose广义逆A+, 1)对A做SVD: A = U S V, 其中 U, V为酉方阵, S为一般对角阵; 2)将S非零元取逆, 零元不变, 然后专置得到一个一般对角阵T; 3)则广义逆为A+ = V* T U, 其中 表示取矩阵的复共轭.
VI.XX. 读取数据
- 从TXT文件中读写数据
1 | #include <stdio.h> |
1 | #include "stdio.h" |
VI.XXI. 输出显示
VI.XXI.I. printf函数
printf函数9是一个标准库函数,它的函数原型在头文件“stdio.h”中。但作为一个特例,不要求在使用 printf 函数之前必须包含stdio.h文件。
printf(“格式控制字符串”, 输出表列)
:其中格式控制字符串用于指定输出格式。格式控制串可由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串,在%后面跟有各种格式字符,以说明输出数据的类型、形式、长度、小数位数等。如:
- “%d”表示按十进制整型输出;
- “%ld”表示按十进制长整型输出;
- “%c”表示按字符型输出等。
VI.XXI.II. 输出数组
先定义这个数组有几个元素,否则会不能现返回值!
下标法
1
2
3
4
5
6
7
8
9
10
11
12
13#include<stdio.h>
void main()
{
int a[5] ;
int n;
printf("Please input words:");
for(n=0;n<5;n++)
scanf("%d",&a[n]);
printf("\n");
for(n=0;n<5;n++)
printf("%d",a[n]);
printf("\n");
}通过数组名计算数组元素地址,找出元素的值
1
2
3
4
5
6
7
8
9
10
11
12
13#include<stdio.h>
void main()
{
int a[5] ;
int n;
printf("Please input words:");
for(n=0;n<5;n++)
scanf("%d",&a[n]);
printf("\n");
for(n=0;n<5;n++)
printf("%d",*(a+i));
printf("\n");
}用指针变量指向数组元素
1
2
3
4
5
6
7
8
9
10
11
12
13#include<stdio.h>
void main()
{
int a[5] ;
int *p,n;
printf("Please input words:");
for(n=0;n<5;n++)
scanf("%d",&a[n]);
printf("\n");
for(p=a;p<(a+10);p++)
printf("%d",*p);
printf("\n");
}
VI.XXI.III. 输出矩阵
1 | #include <stdio.h> |
1 | #include <stdio.h> |
1 | #include <stdio.h> |
1 | #include<stdio.h> |