iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++语言的15个晦涩特性分别是什么
  • 576
分享到

C++语言的15个晦涩特性分别是什么

2023-06-17 07:06:00 576人浏览 安东尼
摘要

本篇文章为大家展示了c++语言的15个晦涩特性分别是什么,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。这个列表收集了 C++ 语言的一些晦涩(Obscure)特性,是我经年累月研究这门语言

本篇文章为大家展示了c++语言的15个晦涩特性分别是什么,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

这个列表收集了 C++ 语言的一些晦涩(Obscure)特性,是我经年累月研究这门语言的各个方面收集起来的。C++非常庞大,我总是能学到一些新知识。即使你对C++已了如指掌,也希望你能从列表中学到一些东西。下面列举的特性,根据晦涩程度由浅入深进行排序

  • 方括号的真正含义

  • 最烦人的解析

  • 替代运算标记符

  • 重定义关键字

  • Placement new

  • 在声明变量的同时进行分支

  • 成员函数的引用修饰符

  • 转向完整的模板元编程

  • 指向成员的指针操作符

  • 静态实例方法

  • 重载++和–

  • 操作符重载和检查顺序

  • 函数作为模板参数

  • 模板的参数也是模板

  • try块作为函数

方括号的真正含义

用来访问数组元素的ptr[3]其实只是*(ptr + 3)的缩写,与用*(3 + ptr)是等价的,因此反过来与3[ptr]也是等价的,使用3[ptr]是完全有效的代码

最烦人的解析

“most vexing parse”这个词是由Scott Meyers提出来的,因为C++语法声明的二义性会导致有悖常理的行为:

// 这个解释正确? // 1) 类型std::string的变量会通过std::string()实例化吗? // 2) 一个函数声明,返回一个std::string值并有一个函数指针参数, // 该函数也返回一个std::string但没有参数? std::string foo(std::string());   // 还是这个正确? // 1)类型int变量会通过int(x)实例化吗? // 2)一个函数声明,返回一个int值并有一个参数, // 该参数是一个名为x的int型变量吗? int bar(int(x));

两种情形下C++标准要求的是第二种解释,即使***种解释看起来更直观。程序员可以通过包围括号中变量的初始值来消除歧义:

//加括号消除歧义 std::string foo((std::string())); int bar((int(x)));

第二种情形让人产生二义性的原因是int y = 3;等价于int(y) = 3;

译者注:这一点我觉得有点迷惑,下面是我在g++下的测试用例:

#include <iOStream> #include <string> using namespace std;   int bar(int(x));   // 等价于int bar(int x)   string foo(string());  // 等价于string foo(string (*)())   string test() {     return "test"; }   int main() {     cout << bar(2) << endl; // 输出2     cout << foo(test); // 输出test     return 0; }   int bar(int(x)) {      return x; }   string foo(string (*fun)()) {     return (*fun)(); }

能正确输出,但如果按作者意思添加上括号后再编译就会报一堆错误:“在此作用域尚未声明”、“重定义”等,还不清楚作者的意图。

替代运算标记符

标记符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor,  xor_eq, <%, %>, <: 和 :>都可以用来代替我们常用的&&, &=,  &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和  ]。在键盘上缺乏必要的符号时你可以使用这些运算标记符来代替。

重定义关键字

通过预处理器重定义关键字从技术上讲会引起错误,但实际上是允许这样做的。因此你可以使用类似#define true false 或  #define  else来搞点恶作剧。但是,也有它合法有用的时候,例如,如果你正在使用一个很大的库而且需要绕过C++访问保护机制,除了给库打补丁的方法外,你也可 以在包含该库头文件之前关闭访问保护来解决,但要记得在包含库头文件之后一定要打开保护机制!

#define class struct #define private public #define protected public   #include "library.h"   #undef class #undef private #undef protected

注意这种方式不是每一次都有效,跟你的编译器有关。当实例变量没有被访问控制符修饰时,C++只需要将这些实例变量顺序布局即可,所以编译器可以对 访问控制符组重新排序来自由更改内存布局。例如,允许编译器移动所有的私有成员放到公有成员的后面。另一个潜在的问题是名称重整(name  mangling),Microsoft的C++编译器将访问控制符合并到它们的name  mangling表里,因此改变访问控制符意味着将破坏现有编译代码的兼容性。

译者注:在C++中,Name Mangling 是为了支持重载而加入的一项技术。编译器将目标源文件中的名字进行调整,这样在目标文件符号表中和连接过程中使用的名字和编译目标文件的源程序中的名字不一样,从而实现重载。

Placement new

Placement new是new操作符的一个替代语法,作用在已分配的对象上,该对象已有正确的大小和正确的赋值,这包括建立虚函数表和调用构造函数。

译者注:placement  new就是在用户指定的内存位置上构建新的对象,这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可。placement  new实际上是把原本new做的两步工作分开来:***步自己分配内存,第二步调用类的构造函数在自己已分配的内存上构建新的对象。placement  new的好处:1)在已分配好的内存上进行对象的构建,构建速度快。2)已分配好的内存可以反复利用,有效的避免内存碎片问题。

#include <iostream> using namespace std;   struct Test {   int data;   Test() { cout << "Test::Test()" << endl; }   ~Test() { cout << "Test::~Test()" << endl; } };   int main() {   // Must allocate our own memory   Test *ptr = (Test *)malloc(sizeof(Test));     // Use placement new   new (ptr) Test;     // Must call the destructor ourselves   ptr->~Test();     // Must release the memory ourselves   free(ptr);     return 0; }

当在性能关键的场合需要自定义分配器时可以使用Placement new。例如,一个slab分配器从单个的大内存块开始,使用placement new在块里顺序分配对象。这不仅避免了内存碎片,也节省了malloc引起的堆遍历的开销。

在声明变量的同时进行分支

C++包含一个语法缩写,能在声明变量的同时进行分支。看起来既像单个的变量声明也可以有if或while这样的分支条件。

struct Event { virtual ~Event() {} }; struct MouseEvent : Event { int x, y; }; struct KeyboardEvent : Event { int key; };   void log(Event *event) {   if (MouseEvent *mouse = dynamic_cast<MouseEvent *>(event))     std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl;     else if (KeyboardEvent *keyboard = dynamic_cast<KeyboardEvent *>(event))     std::cout << "KeyboardEvent " << keyboard->key << std::endl;     else     std::cout << "Event" << std::endl; }

成员函数的引用修饰符

C++11允许成员函数在对象的值类型上进行重载,this指针会将该对象作为一个引用修饰符。引用修饰符会放在cv限定词(译者注:CV限定词有 三种:const限定符、volatile限定符和const-volatile限定符)相同的位置并依据this对象是左值还是右值影响重载解析:

#include <iostream>   struct Foo {   void foo() & { std::cout << "lvalue" << std::endl; }   void foo() && { std::cout << "rvalue" << std::endl; } };   int main() {   Foo foo;   foo.foo(); // Prints "lvalue"   Foo().foo(); // Prints "rvalue"   return 0; }

转向完整的模板元编程

C++模板是为了实现编译时元编程,也就是该程序能生成其它的程序。设计模板系统的初衷是进行简单的类型替换,但是在C++标准化过程中突然发现模板实际上功能十分强大,足以执行任意计算,虽然很笨拙很低效,但通过模板特化的确可以完成一些计算:

// Recursive template for general case template <int N> struct factorial {   enum { value = N * factorial<N - 1>::value }; };   // Template specialization for base case template <> struct factorial<0> {   enum { value = 1 }; };   enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120

C++模板可以被认为是一种功能型编程语言,因为它们使用递归而非迭代而且包含不可变状态。你可以使用typedef创建一个任意类型的变量,使用enum创建一个int型变量,数据结构内嵌在类型自身。

// Compile-time list of integers template <int D, typename N> struct node {   enum { data = D };   typedef N next; }; struct end {};   // Compile-time sum function template <typename L> struct sum {   enum { value = L::data + sum<typename L::next>::value }; }; template <> struct sum<end> {   enum { value = 0 }; };   // Data structures are embedded in types typedef node<1, node<2, node<3, end> > > list123; enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6

当然这些例子没什么用,但模板元编程的确可以做一些有用的事情,比如可以操作类型列表。但是,使用C++模板的编程语言可用性极低,因此请谨慎和少量使用。模板代码很难阅读,编译速度慢,而且因其冗长和迷惑的错误信息而难以调试。

指向成员的指针操作符

指向成员的指针操作符可以让你在一个类的任何实例上描述指向某个成员的指针。有两种pointer-to-member操作符,取值操作符*和指针操作符->:

#include <iostream> using namespace std;   struct Test {   int num;   void func() {} };   // Notice the extra "Test::" in the pointer type int Test::*ptr_num = &Test::num; void (Test::*ptr_func)() = &Test::func;   int main() {   Test t;   Test *pt = new Test;     // Call the stored member function   (t.*ptr_func)();   (pt->*ptr_func)();     // Set the variable in the stored member slot   t.*ptr_num = 1;   pt->*ptr_num = 2;     delete pt;   return 0; }

该特征实际上十分有用,尤其在写库的时候。例如,Boost::python, 一个用来将C++绑定到Python对象的库,就使用成员指针操作符,在包装对象时很容易的指向成员。

#include <iostream> #include <boost/python.hpp> using namespace boost::python;   struct World {   std::string msg;   void greet() { std::cout << msg << std::endl; } };   BOOST_PYTHON_MODULE(hello) {   class_<World>("World")     .def_readwrite("msg", &World::msg)     .def("greet", &World::greet); }

记住使用成员函数指针与普通函数指针是不同的。在成员函数指针和普通函数指针之间casting是无效的。例如,Microsoft编译器里的成员 函数使用了一个称为thiscall的优化调用约定,thiscall将this参数放到ecx寄存器里,而普通函数的调用约定却是在栈上解析所有的参 数。

而且,成员函数指针可能比普通指针大四倍左右,编译器需要存储函数体的地址,到正确父地址(多个继承)的偏移,虚函数表(虚继承)中另一个偏移的索引,甚至在对象自身内部的虚函数表的偏移也需要存储(为了前向声明类型)。

#include <iostream>   struct A {}; struct B : virtual A {}; struct C {}; struct D : A, C {}; struct E;   int main() {   std::cout << sizeof(void (A::*)()) << std::endl;   std::cout << sizeof(void (B::*)()) << std::endl;   std::cout << sizeof(void (D::*)()) << std::endl;   std::cout << sizeof(void (E::*)()) << std::endl;   return 0; }   // 32-bit Visual C++ 2008:  A = 4, B = 8, D = 12, E = 16 // 32-bit GCC 4.2.1:        A = 8, B = 8, D = 8,  E = 8 // 32-bit Digital Mars C++: A = 4, B = 4, D = 4,  E = 4

在Digital Mars编译器里所有的成员函数都是相同的大小,这是源于这样一个聪明的设计:生成“thunk”函数来运用右偏移而不是存储指针自身内部的偏移。

静态实例方法

C++中可以通过实例调用静态方法也可以通过类直接调用。这可以使你不需要更新任何调用点就可以将实例方法修改为静态方法。

struct Foo {   static void foo() {} };   // These are equivalent Foo::foo(); Foo().foo();

重载++和&ndash;

C++的设计中自定义操作符的函数名称就是操作符本身,这在大部分情况下都工作的很好。例如,一元操作符的-和二元操作符的-(取反和相减)可以通 过参数个数来区分。但这对于一元递增和递减操作符却不奏效,因为它们的特征似乎完全相同。C++语言有一个很笨拙的技巧来解决这个问题:后缀++和&ndash;操作 符必须有一个空的int参数作为标记让编译器知道要进行后缀操作(是的,只有int类型有效)。

struct Number {   Number &operator ++ (); // Generate a prefix ++ operator   Number operator ++ (int); // Generate a postfix ++ operator };

操作符重载和检查顺序

重载,(逗号),||或者&&操作符会引起混乱,因为它打破了正常的检查规则。通常情况下,逗号操作符在整个左边检查完毕才开始检 查右边,|| 和  &&操作符有短路行为:仅在必要时才会去检查右边。无论如何,操作符的重载版本仅仅是函数调用且函数调用以未指定的顺序检查它们的参数。

重载这些操作符只是一种滥用C++语法的方式。作为一个实例,下面我给出一个Python形式的无括号版打印语句的C++实现:

#include <iostream>   namespace __hidden__ {   struct print {     bool space;     print() : space(false) {}     ~print() { std::cout << std::endl; }       template <typename T>     print &operator , (const T &t) {       if (space) std::cout << ' ';       else space = true;       std::cout << t;       return *this;     }   }; }   #define print __hidden__::print(),   int main() {   int a = 1, b = 2;   print "this is a test";   print "the sum of", a, "and", b, "is", a + b;   return 0; }

函数作为模板参数

众所周知,模板参数可以是特定的整数也可以是特定的函数。这使得编译器在实例化模板代码时内联调用特定的函数以获得更高效的执行。下面的例子里,函数memoize的模板参数也是一个函数且只有新的参数值才通过函数调用(旧的参数值可以通过cache获得):

#include <map>   template <int (*f)(int)> int memoize(int x) {   static std::map<int, int> cache;   std::map<int, int>::iterator y = cache.find(x);   if (y != cache.end()) return y->second;   return cache[x] = f(x); }   int fib(int n) {   if (n < 2) return n;   return memoize<fib>(n - 1) + memoize<fib>(n - 2); }

模板的参数也是模板

模板参数实际上自身的参数也可以是模板,这可以让你在实例化一个模板时可以不用模板参数就能够传递模板类型。看下面的代码:

template <typename T> struct Cache { ... };   template <typename T> struct NetworkStore { ... };   template <typename T> struct MemoryStore { ... };   template <typename Store, typename T> struct CachedStore {   Store store;   Cache<T> cache; };   CachedStore<NetworkStore<int>, int> a; CachedStore<MemoryStore<int>, int> b;

CachedStore的cache存储的数据类型与store的类型相同。然而我们在实例化一个CachedStore必须重复写数据类型(上面的代码 是int型),store本身要写,CachedStore也要写,关键是我们这并不能保证两者的数据类型是一致的。我们真的只想要确定数据类型一次即 可,所以我们可以强制其不变,但是没有类型参数的列表会引起编译出错:

// 下面编译通不过,因为NetworkStore和MemoryStore缺失类型参数 CachedStore<NetworkStore, int> c; CachedStore<MemoryStore, int> d;

模板的模板参数可以让我们获得想要的语法。注意你必须使用class关键字作为模板参数(他们自身的参数也是模板)

template <template <typename> class Store, typename T> struct CachedStore2 {   Store<T> store;   Cache<T> cache; };   CachedStore2<NetworkStore, int> e; CachedStore2<MemoryStore, int> f;

try块作为函数

函数的try块会在检查构造函数的初始化列表时捕获抛出的异常。你不能在初始化列表的周围加上try-catch块,因为其只能出现在函数体外。为了解决这个问题,C++允许try-catch块也可作为函数体:

int f() { throw 0; }   // 这里没有办法捕获由f()抛出的异常 struct A {   int a;   A::A() : a(f()) {} };   // 如果try-catch块被用作函数体并且初始化列表移至try关键字之后的话, // 那么由f()抛出的异常就可以捕获到 struct B {   int b;   B::B() try : b(f()) {   } catch(int e) {   } };

奇怪的是,这种语法不仅仅局限于构造函数,也可用于其他的所有函数定义。

上述内容就是C++语言的15个晦涩特性分别是什么,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网其他教程频道。

--结束END--

本文标题: C++语言的15个晦涩特性分别是什么

本文链接: https://www.lsjlt.com/news/287884.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++语言的15个晦涩特性分别是什么
    本篇文章为大家展示了C++语言的15个晦涩特性分别是什么,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。这个列表收集了 C++ 语言的一些晦涩(Obscure)特性,是我经年累月研究这门语言...
    99+
    2023-06-17
  • python的13个特性分别是什么
    python的13个特性分别是什么,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。如果你是一个正在学习python的c、c++或者java程序员,或者你是刚开始学python...
    99+
    2023-06-02
  • Linux Mint 15路线图里的3大新特性分别是什么
    本篇文章给大家分享的是有关Linux Mint 15路线图里的3大新特性分别是什么,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Linux Mint 14 (Nadia)的发布...
    99+
    2023-06-16
  • HTMl5中28个新特性分别是什么
    这篇文章主要介绍HTMl5中28个新特性分别是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1. 新的Doctype 尽管使用<!DOCTYPE html>,即使浏览...
    99+
    2024-04-02
  • javascript语言特性是什么
    这篇文章主要介绍javascript语言特性是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!javascript语言特性有:1、解释性执行的脚本语言;2、基于对象的脚本语言;3、简单弱类型脚本语言;4、相对安全脚...
    99+
    2023-06-15
  • Java 9特性的三个新的API分别是什么
    本篇文章给大家分享的是有关Java 9特性的三个新的API分别是什么,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Oracle已经公布,Java9***增强计划集(众所周知的J...
    99+
    2023-06-17
  • Go语言是否兼容C语言的特性
    Go语言是一种由Google开发的编程语言,它具有简洁、高效、并发支持等特点,逐渐受到广泛关注和应用。在很多情况下,开发者可能需要与C语言进行交互或者利用C语言的特性,这时候就需要考虑...
    99+
    2024-03-07
    go语言 特性 兼容性 标准库
  • C++与C语言的区别是什么
    这篇“C++与C语言的区别是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“C++与C语言的区别是什么”文章吧。1. 结构...
    99+
    2023-06-29
  • 值得学习的三个CSS 新特性分别是什么
    今天就跟大家聊聊有关值得学习的三个CSS 新特性分别是什么,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1. 特性查询不久前,我写了 我真心期望的一...
    99+
    2024-04-02
  • Go语言的典型编程语言特性是什么?
    IT行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰。所以我们需要踏踏实实的不断学习,精进自己的技术,尤其是初学者。今天编程网给大家整理了《Go语言的典型编程语言特性是...
    99+
    2024-04-04
  • 15个实用的PHP正则表达式分别是什么
    这篇文章将为大家详细讲解有关15个实用的PHP正则表达式分别是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。对于开发人员来说,正则表达式是一个非常有用的功能,它提供了 查找,匹配,替换 ...
    99+
    2023-06-17
  • FlexSDK4的八大新特性分别是什么
    这篇文章主要为大家展示了“FlexSDK4的八大新特性分别是什么”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“FlexSDK4的八大新特性分别是什么”这篇文章吧。FlexSDK4新特性FlexS...
    99+
    2023-06-17
  • 汇编语言和c语言的区别是什么
    区别:1、因为汇编语言实质上是机器语言的助记符,是直接面对CPU的语言,所以汇编语言的运行效率比C语言高;2、汇编语言对硬件的可操控性强,C语言硬件可操控性比较差;3、汇编语言的目标代码体积小,C语言目标代码体积大;4、汇编语言不易维护,C...
    99+
    2023-05-14
    汇编语言 C语言
  • C语言的概念和特点是什么
    本篇内容介绍了“C语言的概念和特点是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!我们都知道,C语言是一种计算机程序设计语言。它既有高级...
    99+
    2023-06-17
  • java与c语言的区别是什么
    这篇文章将为大家详细讲解有关java与c语言的区别是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。java基本数据类型有哪些Java的基本数据类型分为:1、整数类型,用来表示整数的数据类...
    99+
    2023-06-14
  • c语言中s和c的区别是什么
    c语言中s和c的区别在于输出数据类型:s用于输出字符串,而c用于输出单个字符。此外,s支持宽度指定和精度指定,而c不支持;s支持空格填充,而c不支持。 C语言中s和c的区别 s和c是C...
    99+
    2024-05-12
    c语言
  • 12个有趣的C语言问答分别是怎样的
    本篇文章为大家展示了12个有趣的C语言问答分别是怎样的,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。0,gets() 方法Q:以下代码有个被隐藏住的问题,你能找到它吗?A:这个不显眼的问题就是使用了...
    99+
    2023-06-17
  • Fedora 15 的两项新功能分别是什么
    这期内容当中小编将会给大家带来有关Fedora 15 的两项新功能分别是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。Fedora QA 负责人 rahulsundaram 透露了两条 Fedora ...
    99+
    2023-06-16
  • 15个可提升编码技能的JavaScript工具分别是什么
    这篇文章将为大家详细讲解有关15个可提升编码技能的JavaScript工具分别是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。JavaScript库是一...
    99+
    2024-04-02
  • c语言auto和register的区别是什么
    在C语言中,auto和register是两个关键字,用于声明局部变量。1. auto关键字:auto是默认的存储类别关键字,它用于声...
    99+
    2023-10-12
    c语言
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作