使用 C 和 C++ 的编程语言概念/C++ 编程入门
由于 C++ 与 C 之间存在独特的联系,大多数 C 程序可以编译为 C++ 程序,而无需或仅需少量修改。不过,这种做法(即以 C++ 程序的形式传递 C 程序)并不推荐;只有在需要在 C++ 程序中使用一些现有的 C 代码时才建议这样做。[1]
#include <stdio.h>
int main(void) {
printf(“An example to the usage of C commands in a C++ program\n”);
return 0;
除了 C 风格的注释之外,C++ 还提供了一种可选的单行注释,它用双斜杠与行中的其他内容隔开。[2] 分隔符右侧程序行上的所有内容都被视为注释,编译器会忽略它们。
} // end of int main(void)
C 头文件可以通过两种不同的方式从 C++ 程序中包含:使用其 C 或 C++ 名称。下一行是后者的示例。C 头文件的 C++ 名称以字母 c 为前缀,并删除 .h 文件后缀。
由于 C++ 库名称通常[3] 在命名空间中声明,因此从头文件中包含的名称不可见,除非我们通过 using 指令明确地使它们可见。对于 C 库头文件,这是 std 命名空间。如果未能引入此命名空间中找到的名称,则必须将所有这些名称与它们的命名空间一起使用。也就是说;省略第 2 行需要我们将第 4 行更改为:
std::printf("An example to ...specification\n");
有时,这可能被证明是一种有用的工具。考虑调用一个名为 function4 的函数,它碰巧具有与当前命名空间中的函数相同的名称和签名。根据您是否通过某个 using 指令导入了此函数,简单地发出名称将导致调用当前命名空间中的函数或出现编译时错误。通过范围运算符并明确地在函数名称之前声明命名空间(可能还有类名)可以修复此错误。
示例:范围运算符的使用。 // 没有 using 指令!!! // 下一行将创建一个 Stack 对象,它恰好位于 CSE224::DS 的嵌套命名空间中。 CSE224::DS::Stack<int> int_stack; ... ... int_stack.push(5); ... // 假设 Stack 类有一个名为 st_f 的静态函数。如果没有使用指令,可以按如下方式调用此函数。 ... CSE224::DS::Stack::st_f(...) ...; ...
可以通过在函数和/或类名之前添加范围运算符来明确引用不在特定命名空间中的名称,即全局命名空间中的名称。根据此,如果您想在 C++ 程序中使用 C 风格包含的 printf
,则必须编写以下代码
::printf("printf included a la C lives in the global namespace\n");
最后要注意的是,C++ 风格的 C 功能包含并不启用面向对象风格的编程。您仍然必须像在 C 中一样调用函数;没有办法向对象发送消息。
#include <cstdio>
using namespace std;
int main(void) {
printf("An example to the usage of C commands in a C++ program with a namespace specification\n");
return 0;
} // end of int main(void)
更好的做法是像真正的 C++ 一样使用 C++!在 C++ 头文件 iostream 中可以找到类似于 C 头文件 stdio.h 中的功能。与标准库头文件中声明的其他名称一样,iostream 中的名称在 std
命名空间中声明。
#include <iostream>
using namespace std;
int main(void) {
C++ 编译器在内部将以下行转换为
operator<<(operator<<(cout, “A program with C++ commands”), endl);
其中 cout
是一个 ostream
对象,表示标准输出(类似于 C 中的 stdout),而 endl
是一个 ostream
操纵器,它将一个换行符插入输出流,然后刷新 ostream
缓冲区(类似于 C 中的 '\n'
)。<< 是一个运算符,用于将输出写入某个 ostream
对象。从转换后的行可以看出,它从左到右关联。因此,下一行将消息写入标准输出,然后将换行符追加到末尾。
请注意,<<
运算符连续调用了两次:一次用于输出 "A program with C++ commands"
,另一次用于追加一个行尾字符。也就是说,下一行等效于
cout << "A program with C++ commands"; cout << endl;
它被转换为
operator<<(cout, "A program with C++ command"); operator<<(cout, endl);
这也说明了为什么 <<
运算符必须返回一个 ostream
对象。
cout << “A program with C++ commands” << endl;
return 0;
} // end of int main(void)
下一个程序是一个 C 程序,它原本应该是一个 C 程序;它不是一个被编译为 C++ 程序的 C 程序。
#include <stdio.h>
尽管接下来的两个函数基本上做的是同一件事,但我们既不能将它们合并成一个函数,也不能给它们起相同的名称。这是因为 C 语言不支持模板或重载。
void fint(int ipar) {
printf("In C function fint...Value of ipar: %d\n", ipar);
} /* end of void fint(int) */
void fdouble(double dpar) {
printf("In C function fdouble...Value of dpar: %f\n", dpar);
} /* end of void fdouble(double) */
#include <iostream>
#include <string>
using namespace std;
下面的 extern
声明将 fint
和 fdouble
声明为使用 C 连接方式链接的函数。也就是说,除了可能在前面加上一个下划线之外,不会进行任何名称改编。或者,我们可以这样写
extern "C" void fint(int); extern "C" void fdouble(double);
extern "C" {
void fint(int);
void fdouble(double);
}
有两个名为 f_overloaded
的函数定义:一个接受 int
参数,另一个接受 double
参数。请记住,这是一个 C++ 程序,C++(由于一个名为“名称改编”的过程)支持函数名重载。这是通过在函数名中编码参数类型并将此转换后的名称传递给链接器来实现的。这使得类型安全链接不再成为问题。例如,下面第一个函数的名称将被[编译器]转换为 f_overloaded__Fi
,而第二个函数将被转换为 f_overloaded__Fd
。[4] 也就是说,链接器会看到两个不同的函数名。
虽然[不像 Java]返回值类型在区分重载函数时会被考虑,但应该非常谨慎地使用此属性。这是因为从 C++ 函数返回的值可以被忽略;一个函数可能被调用只是为了其副作用。下面的代码片段应该可以使这一点更清晰。
int f(int i) { ... } ... void f(int j) { ... } ... // 正常。编译器可以从上下文中轻松推断出程序员的意图。 int res = f(3); ... // 模棱两可!!!用户可能打算调用第二个函数或第一个函数以获得其副作用。 f(5);
void f_overloaded(int ipar) {
cout << "In C++ function f_overloaded..."
<< "The value of ipar: " << ipar << endl;
} // end of void f_overloaded(int)
void f_overloaded(double dpar) {
cout << "In C++ function f_overloaded..."
<< "The value of dpar: " << dpar << endl;
} // end of void f_overloaded(double)
C++ 通过模板机制使类型参数化。[5] 程序员对函数接口(参数和返回值类型)中的所有类型或部分类型进行参数化,而函数体保持不变。
与重载不同,模板机制不需要程序员提供多个函数定义;函数的实例由编译器构建。此过程称为“模板实例化”。它作为调用函数模板或获取函数模板地址的副作用隐式地发生。
template <class Type>
void f_template(Type par) {
cout << "In C++ function f_template..."
<< "The value of par: " << par << endl;
} // end of void f_template(<class>)
int main(void) {
与 C 不同,C++ 允许将声明语句与可执行语句混合使用。这意味着现在可以在标识符的第一个引用点之前声明它们;不需要在进入相关块时声明它们。[6]
int i = 10;
fint(i);
double d = 123.456;
fdouble(d);
string s = "A random string";
根据参数类型,将在编译时解析要调用的函数。不要忘记:我们在这里做的是调用一个预先存在的函数,这些函数碰巧具有相同的名称。
f_overloaded(i);
f_overloaded(d);
以下每个调用都会导致编译器构建不同的实例;如果没有调用,编译器将不会进行任何实例化。
f_template(i);
f_template(d);
f_template(s);
return 0;
} // end of int main(void)
运行程序
[edit | edit source]gcc –c C_file.c↵ # 使用 DJGPP-gcc gxx -o C++_Linking_With_C C++_Linking_With_C.cxx C_File.o↵
在其他有 GNU 编译器集合移植版的环境中,例如 Linux 和 Cygwin,你可能会看到一条消息,提示命令无法识别。在这种情况下,请尝试类似 g++
或 gpp
的命令。
C++_Linking_With_C↵ 在 C 函数 fint...ipar 的值为:10 在 C 函数 fdouble...dpar 的值为:123.456000 在 C++ 函数 f_overloaded... ipar 的值为:10 在 C++ 函数 f_overloaded... dpar 的值为:123.456 在 C++ 函数 f_template... par 的值为:10 在 C++ 函数 f_template... par 的值为:123.456 在 C++ 函数 f_template... par 的值为:一个随机字符串
默认参数
[edit | edit source]#include <iostream>
using namespace std;
传递给某些函数的参数在大多数情况下可能具有某些预期值,而在特殊情况下,它们可能假设不同的值。对于这种情况,C++ 提供了使用“默认参数”作为选项。例如,考虑一个用于打印整数的函数。为用户提供打印整数的基数选项似乎很合理,但在大多数程序中,整数将被打印为十进制整数。使用此功能打印整数的函数将具有以下原型
void print_int(int num, unsigned int base = 10);
只可以为尾部的参数提供默认参数。 retType f(argType1 arg1, ..., argTypen argn = def_value); // 正常 retType f(argType1 arg1, ..., argTypem argm = def_value, ... , argTypen argn); // 错误!!!
默认参数的效果也可以通过重载来实现。上面的 print_int
函数可以用以下函数来表达
void print_int(int num, unsigned int base); void print_int(int num);
int greater_than_n(int *ia, int size, int n = 0) {
int i, count = 0;
for (i = 0; i < size; i++)
if (ia[i] > n) count++;
return count;
} // end of int greater_than_n(int[], int. int)
int main(void) {
int inta[] = { 1, 2, -3, 6, -10, 0, 7, -2};
cout << "The count of numbers greater than 5: "
<< greater_than_n(inta, 8, 5) << endl;
cout << "The count of positive numbers in the sequence: "
<< greater_than_n(inta, 8) << endl;
return 0;
} // end of int main(void)
C++ 引用
[edit | edit source]#include <iostream>
以下 using 指令与我们之前见过的不同。它不用于引入命名空间中找到的所有名称,而是引入命名空间中的特定类;它将引入 std 命名空间中的 iostream 类,而同一命名空间中的所有其他类/函数都将不可见,因此只能在作用域运算符的帮助下使用。
using std::iostream;
以下行是 C++ 中引用用法的一个示例。引用用于提供按引用调用语义。[7] 它们帮助程序员编写比使用指针编写的代码更简洁、更容易理解的代码。通过指针模拟的按引用调用语义以及所有相关工作都由编译器完成[就像 Pascal 中的 var 参数或 C# 中的 ref 参数一样]。
实际上,引用可以看作是另一个变量的别名。在传递参数时,形式参数成为对应实际参数的别名。从技术上讲,它是一个指向另一个变量的常量指针。因此,引用必须在其定义点进行初始化。[8] 也就是说,
int ivar = 100; ... // 编译器会将下一行代码转换为 int *const iref = &ivar; // int *const iref = &ivar; int &iref = ivar; // 正确。 int &iref2; // 错误
一旦定义为引用,就不能再引用其他变量。 也就是说,根据上面的定义,
int ivar2 = 200; ... iref = var2; // 将转换为 *iref = var2;
不会使 iref
成为 ivar2
的别名。 它将设置 iref
,并通过 iref
设置 ivar
为 200
。
相应地,以下函数的转换后的代码如下所示
void swap(int *const x, int *const y) { int temp = *x; *x =*y; *y = temp; } // void swap(int *const, int *const) 的结束
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
} // end of void swap(int&, int&)
除了其不可否认的灵活性之外,C/C++ 中数组和指针的特殊关系有时会导致难以发现的运行时错误。 例如,以下等效声明。
long sum(int arr[5]); long sum(int arr[]); long sum(int *arr);
编译器最终会将前两个声明转换为第三个声明,这意味着我们可以传递任何长度的数组。 这与第一个声明的意图形成鲜明对比。 因此,程序员和用户都应该更加努力地避免任何可能的运行时错误,例如使用越界索引值。
下一行是一个类型定义,我们将使用它来对数组参数进行更严格的类型检查。 它定义了一个对五个 int
的数组的引用。 任何声称此类型的数组标识符不仅会检查其组件类型,还会检查其长度。 例如,任何尝试将大小不为五的数组传递给 sum
的尝试都将被视为编译时错误。
应该强调的是,这对那些在编译时可以确定大小的数组有效。 C++ 编译器不会将任何运行时检查(这会降低程序速度,因此与 C/C++ 的设计理念不符)合并到生成的代码中。 出于这个原因,以下片段甚至无法编译。
long sum(int size) { // size 的值取决于传递的参数。 因此,la 的长度将在运行时确定。 int la[size]; array_of_five_ints a = la; ... } // long sum(int) 的结束
#define NO_OF_ELEMENTS 5
typedef int (&array_of_five_ints)[NO_OF_ELEMENTS];
long sum(array_of_five_ints arr) {
long res = 0;
for (int i = 0; i < NO_OF_ELEMENTS; i++) res += arr[i];
return res;
} // end of long sum(array_of_five_ints)
既然引用是一个常量指针,那么下面的内容可以看作是
typedef int (*const rf)(int);
也就是说,以下 typedef
定义了函数引用类型的同义词 rf
,它接受一个 int
并返回一个 int
。 换句话说,任何接受一个 int
并返回一个 int
的函数,例如 multiply_with_3
或 raise_to_the_3rd_power
,都可以被视为 rf
的实例。
typedef int (&rf)(int);
int multiply_with_3(int i) {
cout << "Tripling " << i << ": ";
return 3 * i;
} // end of int multiply_with_3(int)
int raise_to_the_3rd_power(int i) {
cout << "Raising " << i << " to the third power: ";
return i * i * i;
} // end of int raise_to_the_3rd_power(int)
下一个函数展示了在 C++ 中实现回调机制的另一种方法。 f_caller
生成的副作用取决于传递给它的函数作为其参数。
void f_caller(rf f) {
cout << "In the f_caller..." << f(5) << endl;
} // end of void f_caller(rf)
int main(void) {
int a = 5, b = 3;
cout << "TESTING CALL-BY-REFERENCE" << endl;
cout << "a: " << a << "\tb: " << b << endl;
没有地址运算符! 编译器会处理所有事情。[9] 用户只需要知道的是,函数中发生的副作用是永久性的。
swap(a, b);
cout << "a: " << a << "\tb: " << b << endl;
cout << "TESTING ARRAYS WITH SIZE INFORMATION" << endl;
int ia[] = {1, 3, 5, 7, 9};
cout << "Sum of array elements: " << sum(ia);
cout << "TESTING CALLBACK" << endl;
f_caller(multiply_with_3);
f_caller(raise_to_the_3rd_power);
return 0;
} // end of int main(void)
流操纵器
[edit | edit source]C++ 中的所有流对象,例如 cout 和 cin,都维护一个状态信息,可以用来控制输入/输出操作的细节。 这包括精度等属性浮点数、表格数据的宽度等。 以下是一个简单的程序来演示 C++ 中的一些操纵器。
#include <fstream>
#include <iomanip>
#include <iostream>
using namespace std;
int main(void) {
int i;
ofstream outf("Output.dat");
cout << "Enter an int value: "; cin >> i;
outf << "Number entered: " << i << endl;
一个操纵器修改流对象的内部状态并导致后续的输入/输出以不同的方式执行; 它不会写入或读取底层流。 例如,以下语句中的 setw
为下一个参数的输出预留了与参数中传递的值一样多的字符空间; left
以左对齐方式写入所有后续输出(直到它被另一个操纵器(如 right
)更改)。
outf << setw(12) << left << "Hex"
<< setw(12) << " Octal"
<< setw(12) << " Dec" << endl;
如果生成的输出没有填满为它预留的所有空间,我们选择用下划线字符填充剩余的空位; 写入十二个字符窗口的任何整数都会以右对齐的方式使用十六进制表示法写入,并通过 showbase
操纵器通过在输出之前添加前缀的方式传递给用户。
outf.fill('_');
outf << right << setw(12) << hex << showbase << i;
outf << " " << setw(12) << oct << i;
outf << " " << setw(12) << setbase(10) << /* noshowbase << */ i << endl;
bool bool_value = true;
outf << endl << "bool_value: " << boolalpha << bool_value
<< '\t' << noboolalpha << bool_value << endl;
下一行使用默认精度值初始化局部变量精度。 这恰好是 6,这意味着在小数点后写入六位数字。 如果你想要更高的精度,你可以将它作为参数传递给同一个函数,或者以类似的方式使用 setprecision
。
int precision = outf.precision();
double d, divisor;
do {
cout << "Enter a double value: "; cin >> divisor;
if (divisor == 0) break;
outf << endl << "Double value: " << divisor << endl;
d = 1 / divisor;
while (divisor != 0) {
outf << "Precision: " << precision << "... d: " << fixed << d;
outf << " Using sci. notn.: " << scientific << uppercase << d << endl;
cout << "New precision: "; cin >> precision;
if (precision != 0) {
outf << "New precision: " << precision;
outf << setprecision(precision);
} else break;
} // end of while(divisor != 0)
precision = outf.precision();
} while (divisor != 0);
return 0;
} // end of int main(void)
运行程序
[edit | edit source]gxx -o Test_Manipulator.exe Manipulators.cxx↵ # 使用 DJGPP-gcc Test_Manipulator > Output.dat↵ 输入一个 int 值: 12345↵ 输入一个 double 值: 5.6↵ 新精度: 15↵ 新精度: 16↵ 新精度: 17↵ 新精度: 18↵ 新精度: 19↵ 新精度: 0↵ 输入一个 double 值: 4.56↵ 新精度: 18↵ 新精度: 17↵ 新精度: 16↵ 新精度: 15↵ 新精度: 0↵ 输入一个 double 值: 0↵
输入的数字: 12345 十六进制 八进制 十进制 ______0x3039 ______030071 _______12345 bool_value: true 1 双精度值: 5.6 精度: 6... d: 0.178571 使用科学记数法: 1.785714E-01 新精度: 15 精度: 15... d: 0.178571428571429 使用科学记数法: 1.785714285714286E-01 新精度: 16 精度: 16... d: 0.1785714285714286 使用科学记数法: 1.7857142857142858E-01 新精度: 17 精度: 17... d: 0.17857142857142858 使用科学记数法: 1.78571428571428575E-01 新精度: 18 精度: 18... d: 0.178571428571428575 使用科学记数法: 1.785714285714285754E-01 新精度: 19 精度: 19... d: 0.178571428571428575 使用科学记数法: 1.785714285714285754E-01 双精度值: 4.559999999999999609E+00 精度: 19... d: 0.219298245614035103 使用科学记数法: 2.192982456140351033E-01 新精度: 18 精度: 18... d: 0.219298245614035103 使用科学记数法: 2.192982456140351033E-01 新精度: 17 精度: 17... d: 0.21929824561403510 使用科学记数法: 2.19298245614035103E-01 新精度: 16 精度: 16... d: 0.2192982456140351 使用科学记数法: 2.1929824561403510E-01 新精度: 15 精度: 15... d: 0.219298245614035 使用科学记数法: 2.192982456140351E-01
注释
[edit | edit source]- ↑ 即使在这种情况下,也可能存在更好的解决方案,我们将在后面的“链接 C 和 C++ 程序”中介绍。
- ↑ 在许多编译器中得到了广泛支持,这现在是 C 编程语言的标准功能。
- ↑ 也就是说,你仍然可以在不将编程实体(例如:类、函数等)放在特定命名空间的情况下编写 C++ 程序。在这种情况下,这些实体被称为放置在全局命名空间中。然而,这种风格可能会导致名称冲突问题,这是由于同一个命名空间中的实体不能拥有相同的名称。如果你可以访问源代码,这个问题可以通过更改相关实体的名称并使它们唯一来解决。但这在没有源代码的情况下是行不通的。在这种情况下,解决方案是使用命名空间。
- ↑ 请注意,没有标准的方法来破坏函数名;编译器编写者可以自由选择自己的方案。
- ↑ 除了函数外,还可以参数化类。有关更多信息,请参阅参数化类型章节。
- ↑ 如果你认为这听起来不太对,很可能是你一直在使用支持语言扩展的 C 编译器,这意味着移植你的代码可能是一项艰巨的任务。如果你不相信,尝试将 -pedantic(在 gcc 中)或 /Tc(在 MS Visual C/C++ 中)添加到你的命令行并看看会发生什么!
- ↑ 在其他情况下,使用引用可以使生活更轻松。有关更多信息,请参阅基于对象的编程章节。还应该注意,正如继承讲义中将要展示的那样,通过引用和指针可以实现动态分派。
- ↑ 记住初始化和赋值之间的区别?常量必须在其创建时赋予一个值;它不能在没有初始值的情况下创建。它也不能被赋予一个新值,这解释了为什么引用(由编译器管理的常量指针)在初始化后不能被修改为引用另一个变量。
- ↑ 也就是说,编译器将默默地将此行转换为
swap(&a, &b);