870920 Menu

SwingCoder之C++备忘录·3

代码流程与控制结构

C++头文件不参与编译,通常仅供源文件就地展开(include预编译指令可就地展开头文件中的所有内容),cpp源文件参与编译,通常,一个cpp源文件视为一个编译单元。无论头文件,还是源文件,C++文件中的代码均无分行的概念,空格并不视为编译元素。分行、空格和缩进等只是写代码时的排版格式,便于清晰直观的查看。C++代码中,参与编译的每条语句以“;”作为本条语句的结束符(一条语句可以包含多个表达式和函数调用)。而一对花括号(“{”和“}”)则构成一个代码块,该代码块中可包括一到多条语句。代码块同时也构成一个局部作用域。此作用域内声明的栈变量,在代码执行到作用域结束处时自动被系统销毁。

代码文件中的预编译指令、宏定义、函数定义中的函数头、类定义中的类头、类定义中的访问权限标识符、函数中goto跳转到的目标位置标识符、if、while、for、switch、case等等流程控制标识符及其附属的表达式,这些不属于C++语句,它们不以“;”为结束符。或者这么认为:它们和后面的语句共同构成一条具有控制结构的复合型语句。

C++共有7种形式的控制语句:顺序(从调用位置起,由上到下逐行执行),if,if…else,switch,while,do…while,for。可归纳为:顺序,选择和迭代。这7种控制语句只能以先后或嵌套的方式顺序执行,从而构成一个个的执行代码块或函数(结构化编程)。而多线程编程中,多个函数或代码块则可以“并发”或“轮发”执行。

 控制结构中,均必须至少有一条语句。可以只有一个分号(空语句),或者写一个空内联函数或空宏:doNothing(); 该宏的定义为:#define doNothing()

 无论是选择(if)还是循环(for,while),多层嵌套不利于维护和理解,尽量不要超过3层。

 关于switch,即使确信没有遗漏,也应提供default/break、给出提示或断言。

 其他控制结构还有:break, continue, return, throw-try-catch, goto, 函数递归。

数组与函数

数组是具有固定大小、相同类型和连续属性的数据结构(整个数组占据一个独立的内存块,即一块内存中存储了多个连续排列的同类型数据)。数组名即为该数组的第一个元素的内存地址,即:数组名即该数组的常量指针(const指针,指针所保存的地址恒定不变,该地址的内容可变)。获取数组中某个元素内容的下标方括号是C++的运算符之一,该运算符与圆括号具有相同的优先级,可以重载。数组元素是指针时,对该指针取值,需使用圆括号:*(myArray [i])。

数组的大小n表示该数组有n个元素,这n个元素的索引依次为:[0]到[n-1]。声明数组时,如果给出完整的初始化列表(右花括号后以分号结尾:int array[] = {1, 2, 3…};),则无需再指定数组的大小,编译器会计算出元素的个数。如果指定数组的大小,则必须用数值字面值或无符号整型常量。static性质的数组,如果未显式指定元素的值,编译器会自动将每个元素的值置为0、空指针或默认构造函数的结果。非static性质的数组,如果未初始化,则其元素的值不确定。

string字符串的本质也是数组(以字符’\0’为结束符的字符数组,即char[]或char*)。

C++语言本身并没有数组的边界检查机制,因此操作数组时需确保不要越界,无论是下标法索引值访问,还是使用指针的算术运算进行访问。下标索引值可以用表达式,只要运算结果是非负整数即可,因此,最好使用size_t类型,而非int。还需注意:字符型数组的最后一个元素为’\0’。如果没有该元素,C++无法确定字符数组的有效长度。

函数内的局部数组可声明为static性质的静态数组,这样,仅在第一次调用该函数时初始化该数组,并且该数组与其元素的值将常驻内存中,这样可以提高执行效率,或者用于特殊目的。并且,与普通数组不同的是,static静态数组在首次创建时,即使不进行各个元素的显式初始化,编译器也会把该数组的每个元素初始化为默认值。

数组作为函数参数时,C++内部按传递地址的方式进行传参。声明该函数的语法为:

// 1参为数组第一个元素的地址
void modifyArray (float* myArray, const int arraySize);

// 也可以这样声明
void modifyArray (float myArray[], const int arraySize);

如要确保函数中不修改数组,则1参加const前缀:
void doSomethingFromArray (const float* myArray, const int arraySize);

一维数组可视为数据链,二维数组可视为数据表,或者理解为包含数组的数组、二维矩阵、行列式、多条数据链等。它以行和列的形式进行声明:array[行数][列数](假设行数为3,列数为4,则array数组相当于一个包含3个float数组的数组)。不能声明为array[x, y](此时,根据逗号运算符的特性,该数组实际上视为array[y])。数组float a[3][4]可以用下表来理解和对照。其第0行第0列(即第一个元素)的地址为:float** a,亦即&(a[0][0])或&a[0]。

float a[3][4] 第0列 第1列 第2列 第3列
第0行 a[0][0] a[0][1] a[0][2] a[0][3]
第1行 a[1][0] a[1][1] a[1][2] a[1][3]
第2行 a[2][0] a[2][1] a[2][2] a[2][3]

表 1 4 二维数组相当于一个数据表,以行和列的形式保存数据

数组与指针密切相关,声明一个数组时,也相当于同时声明了指向该数组第一个元素的常量指针(不能再指向其它内存):

// a既是数组名,也是指针名,即:double* const a == &a[0],*(a + i) == a[i]
double a[10];

 函数是典型的结构化编程方式,也是类的行为和功能的具体承载者。

 函数可以返回一个值(对象、变量、指针或引用,由函数声明中的返回类型确定),也可以不返回值,单纯的执行一段代码(返回类型为void)。还可以返回两个或更多的值(更准确的说法是改变两个或更多已有的值),此时形参必须为非const指针或引用,并且必须提前定义需修改的实参(非常量)。

 函数形参中的const指针或const引用,意味着此函数不会修改该形参,反之,则意味着此函数可能或者必将修改该形参。对于const形参,可传入const实参,也可传入非const实参。

 函数形参中的非const指针或非const引用,在调用此函数时,只能传入非const实参,不能传入const实参。比如:char* c = “abcd”; c其实是指向const类型的字串数据,如果将c作为实参传给函数形参中的非const指针,编译器会报错。非const形参往往意味着本函数将修改该参数,见上一条。但可以使用C++ 11的右值引用来解决此问题。

 函数的重载与其返回类型无关,只与参数的类型与个数有关。使用重载函数,需注意实参的二义性问题,即:编译器不知道该调用哪个重载函数。

 函数模板的模板参数,不仅可以是某个类型,还可以是某个类型的指针。类模板同理。调用模板函数时,指针型模板参数,必须要在类型名之后加“*”。比如:

// 父类指针转子类指针
MysubType* subPtr = static_cast<MySubType*>(superPtr);

 某个函数的函数名,即该函数的函数指针。回调函数正是通过函数指针来实现的:
void callbackFunc(Type a, Type b); // 需回调的函数原型

// 1参为函数指针,2参和3参为1参函数的实参
void myFunction(callbackFunc, a1, b1);

 C++非虚函数的执行效率与普通C函数是完全一致的。如果一个类声明了虚函数,那么这个类的每个对象都会有一个指向虚函数表的指针,这是一个被隐藏的数据成员。

 虚函数无法内联,因为内联函数的实质是编译时展开,而虚函数则是运行时动态确定和调用。

 成员函数的4种形式:重载(同一个类中,相同函数名,形参的个数与类型不同)、重写(子类重写父类的虚函数,执行不同的操作和功能。子类可在重写的函数中调用父类的同名函数,注意要前缀“基类::”。重写的函数具有多态性)、覆盖(子类改写父类的同名非虚函数,无多态性)、实现(子类继承并实现父类的纯虚函数)。

 类的静态函数不属于成员函数,它属于类本身,由该类的所有对象所共享(静态数据成员同理)。

 仿函数并非函数,而是对象,该对象所属的类重载了operator()运算符。

成员函数的串联调用:
Time time(0, 0, 0);
time = time.setHour(10).setMinute(55).setSecond(25);

类的成员函数实现串联调用基于从左到右的结合律。实现要点为:需串联调用的成员函数的返回类型为该类的栈对象、引用对象或指针(返回指针时,需用->运算符调用后续成员函数)。比如:
Time& setHour(int h)
{
hour = (h >= 0 && h < 24) ? h : 0; // hour为Time类的数据成员
return *this; // 修改自身后返回
}

注意,串联调用中的最后一个函数,其返回类型可以不遵循此规则。但是,除此之外,串联调用中的其他成员函数,必须遵循上述规则。函数串联调用的另一例:

cout << int_a << double_b << “Text…” << float_c << endl;

上条语句也属于串联调用:cout是输出流类的对象,<<是该类的运算符重载。

递归:递归是一种多层次控制结构(相比于顺序、选择、迭代),它更像一种算法思路,而不仅仅是函数内调用自身这种简单的语法形式。如果把一个函数视为一个解决问题的过程,那么可以这样描述递归的特点:问题可以细分为算法相同、形式相同的多个部分,最后一个部分不能再细分。每个部分均由该过程(函数)负责解决,同时将问题进一步细分,即每次解决,除了解决自身所负责的部分之外,还需不断逼近最底层的最后部分。无法再进一步细分的最底层部分得以解决后,该过程开始逐级返回,每一层的返回值作为上一层调用的参数,直至最顶层调用。

递归是人工智能(AI)领域的重要工具,在数据结构与算法中(比如二叉树的遍历与排序、二分查找等)也有广泛的应用。很多情况下,使用递归可以大大简化问题的复杂性和迭代的繁琐性(递归可以替换为迭代)。

 层次较多时,不适于用递归来解决,因为压栈太多,可能耗尽资源。迭代可能更适合。

 一个函数中,不仅可以调用一次“自己”,还可以多次调用自己,即包含多个递归

 情况完全相同的分治策略,最适于用递归思路来解决

 编译器有可能将递归优化为迭代。因此,编程时,能用递归就不用迭代。