C++ 语法揭秘






3.50/5 (6投票s)
解答初学者甚至高级程序员心中关于奇怪语法的许多疑问
引言
一些初学者认为 C++ 语法毫无价值,在这篇文章中,你将了解到为什么保持现状是有逻辑的。本文也可适用于 C# 和 JAVA,因为它们的语法与 C++ 语法相似。
介绍这种语法逻辑的最佳方式是通过问答形式,让我们开始吧。
致谢
感谢 Andrew Phillips 帮助我修正关于 switch 语句的一些事实。C++ 语法揭秘
为什么 C++ 要求程序员在函数定义中使用花括号,即使函数体可能只有一个简单的语句?换句话说,为什么函数体需要花括号,而 if 语句体不需要?
假设你有一个空的函数体,你应该如何书写?!!在 C++ 语法中,你应该这样写:
void fn() {}
如果函数体语法像 if 语句语法,你将不得不这样写:
void fn();
分号是必需的,用来告诉编译器语句的结束。对于 for 循环语句的空体和 if 语句的空体都是如此。但如果你站在编译器的角度思考,你就会感到困惑;这是一个空的函数体还是只是一个原型?或者如果编译器在类定义中找到这个语句,编译器应该如何处理?如果它是一个空的函数,编译器应该在找到这个函数的调用时生成内联代码,而如果它是一个原型,编译器应该在某处查找定义,并在找到这个函数的调用时生成一个正常的函数调用。另一种情况是,编译器是否应该因为缺少这个函数的定义(这是原型)而将这个类视为抽象类,还是因为存在函数定义(这不是原型)而将其视为具体类?!!(不不不,这不是原型)。实际上,仅仅考虑函数定义和函数原型之间的歧义就令人头疼。我认为我已经引起了足够的头痛。
为什么 try-catch 块会有花括号?
首先,我们来谈谈 try 块。因为 try 和 catch 是相互关联的,我们把它们看作一个单一的语句,也就是说,其中一个存在而另一个不存在是没有意义的。此外,你必须记住,一个 try 块可以有多个 catch 块来正确处理不同类型的多个异常。现在假设你有以下代码:
...
try { // if this brace is not exist, ..... read next comment
...
try {
...
}
catch(except &e1) {
...
}
catch(except &e2) { // is this the 2nd catch for 2nd
//try, or 1st catch for 1st try
... // only the above brace can resolve this ambiguity
}
...
} // thank you brace, now I know that the previous two catch
// statements are for the nested try-block
我认为代码本身就能说明问题。再次,只需站在编译器的角度思考,你就会发现自己很困惑,因为如果缺少花括号,代码将变得含糊不清。其次,catch 块,嗯,看看下面的代码:
...
try {
...
}
catch(except &e1) { // if this brace is not exist, .......
// read the following comment
...
try {
...
}
catch(except &e2) {
...
}
catch(except &e3) { // is this 2nd catch for 2nd try, or 2nd
// catch for 1st try???
... // only the above brace can resolve this ambiguity
}
} // thank you brace, now I know that the previous two catch
//statements are for nested try-block
现在清楚了吗?
为什么我们需要 typename 关键字,而 class 关键字可以替代它?
实际上,class 关键字并非在所有地方都能替代 typename 关键字。它只在模板定义中可以。
template <class T>
T fn(){}
template <typename T>
T fn(){}
在许多情况下,你必须使用 typename 关键字。我只举一个例子。假设你有一个外部类,其中包含一个内部的 struct 或 class(组合,而非继承)。现在,假设你想声明/定义一个指向这个嵌套类型对象的指针,你应该如何编写?你可以这样写:
OuterType::InnerType *someObject;
但是,当你阅读这一行代码时,你有没有站在编译器的角度想过?如果是这样,你应该感到困惑。这是一个尝试将 someObject 与静态数据成员 InnerType 相乘的表达式?还是一个指向 InnerType 类型的指针的声明?回想一下,当你有一个静态数据成员时,你可以通过输入类名后跟作用域解析运算符:: 再跟成员名来调用它。为了解决这种歧义,你必须使用 typename 关键字,如下所示:
OuterType::InnerType *someObject;
实际上,这仅在你尝试定义模板时是必要的,如下所示:
template <typename OuterType>
void foo() {
typename OuterType::InnerType *innerPtr;
// do something with this pointer
}
David Vandevoorde 和 Nicolai M. Josuttis 在他们非常有价值的书《C++ 模板:完全指南》中提到了四种必须使用 typename 的情况。我认为这段代码足以说明这个关键字的必要性。我不会深入解释模板。
为什么类类型定义需要结束分号,而函数定义不需要?
嗯,类类型定义主要用于定义其类型的对象(我知道它可能是抽象类,但我说的是一般情况),因此用分号结束声明语句。换句话说,类定义中的花括号表示定义在 RAM 中分配的对象大小的块的开始和结束,而不是声明语句的结束。你见过这样的代码吗:
class Person {
public:
string name;
int age;
bool male;
....
}per; // NOTICE: declaration seen here
如你所见,这里的花括号表示块的边界,以便我们确定该数据类型的大小,但声明还没有结束,它在结束花括号之后显示对象的名称“per”,后面跟着一个分号来表示声明语句的结束。当你省略对象名称时,编译器会简单地理解你现在不需要声明。函数不需要这个,因为它们不用于声明任何东西。另一种迫切需要使用分号的情况是匿名类类型。它们是名称为空的类/结构/联合,只有主体,并且概念上必须有一个声明。
struct {
int a;
bool b;
}var1;
为什么 switch 语句在每个 case 之后都需要 break;?换句话说,为什么执行不会在适当的 case 之后停止?
实际上,这非常有用,不像你想象的那样。在某些情况下,程序员希望在多个条件发生时执行一段代码。例如,假设你有一个选项菜单并读取输入:
swith(input) { // asks for
case 'c':
case 'C':
cleanAllPointers(); break;
}
如果情况像问题中那样,代码应该改为:swith(input) { // asks for
case 'c':
cleanAllPointers();
case 'C':
cleanAllPointers();
}
如果你即将编写的代码太长,那将非常痛苦,这里只是一个函数调用,你可以想象更糟糕的情况。请记住,case 只是标签,它们是常量值,你不能创建复杂的表达式,例如:case ('c' || 'C'): // this cannot be a label since
// labels have no spaces in their names
cleanAllPointers();
因此,C 的创建者不得不做出权衡:- 是重复代码并停止执行,还是
- 通过只重复 break; 来节省代码重复?
case 1:
do_something_a;
case 2:
do_something_b; break;
另一方面,如果 switch 语句在执行完适当的 case 标签后自动中断,你应该这样编写:case 1:
do_something_a; do_something_b;
case 2:
do_something_b;
我不会让你想象如果 something_b 是一段复杂的代码;我只想说,如果 something_b 只是一段简单的 try/catch 块,那代码会是什么样子?太长了!
为什么函数指针需要过多的括号?!
这看起来是个难题,但实际上,它是本文中答案最短的问题。答案是“复习 C++ 优先级表”。就是这样。让我们多解释一下。检查下面的简单代码:cout << 3 + 4 * 2; // results in 11
这段代码的结果是 11,因为 operator* 的优先级高于 operator+。如果你想让加法先于乘法发生,你会怎么做?你可以简单地使用括号,像这样:cout << (3 + 4) * 2; // results in 14
关键在于 operator() 的优先级高于 C++ 的所有其他运算符——除了 operator[] 具有相同的优先级。括号内的任何内容都应先计算。回到函数指针。假设你想声明一个指向函数的指针,该函数接受 int 参数并返回 char,你可以这样写:char * ConvertAscii(int ascii); // this is a function
// returns char* not pointer to function returns char
这是因为编译器会根据以下形式检查你的声明:return-type identifier(parameter-list);
然后,它会发现 char* 是一个类型。关键是把 char 和 operator* 分开。使用括号允许你告诉编译器“嘿,operator* 不是返回类型的一部分”,就像这样:char (*ConvertAscii)(int ascii); // this is a pointer to
// function takes int and returns char
用很多解释来阐述简单的想法是很正常的。
参考文献
- C++ 模板:完全指南。作者:David Vandevoorde, Nicolai M. Josuttis。关于 typename/class 的问题。