17

在与我团队中一位新来的开发人员讨论后,我意识到在 C++ 中仍然存在使用 C 构造的习惯,因为它们应该更好(即更快、更精简、更漂亮,选择你的理由)。

与类似的 C++ 构造相比,哪些示例值得分享,展示了 C 构造?

对于每个示例,我都需要阅读 C++ 构造与原始 C 构造一样好甚至更好的原因。目的是为一些在 C++ 代码中被认为有些危险/不安全的 C 构造提供替代方案(只要明确标记为 C++0x,就接受仅 C++0x 有效的答案)。

我将在下面发布一个答案(结构内联初始化)作为示例。

注1:请,每个案例一个答案。如果您有多个案例,请发布多个答案

注 2:这不是 C 题。不要在这个问题中添加“C”标签。 这不应该成为 C++ 和 C 之间的斗争。仅研究 C++ 的 C 子集的一些构造,以及它们在其他 C++“工具包”中的替代方案

注 3:这不是一个 C-bashing 问题。我要理由。吹嘘、抨击和未经证实的比较将被降低。在没有 C 等价物的情况下提及 C++ 功能可能会被认为是题外话:我希望将 C 功能与 C++ 功能并排放置。

4

21 回答 21

34

RAII 和所有随之而来的荣耀与手动资源获取/释放

在 C 中:

Resource r;
r = Acquire(...);

... Code that uses r ...

Release(r);

例如,Resource可以是指向内存的指针,Acquire/Release 将分配/释放该内存,或者它可以是一个打开的文件描述符,Acquire/Release 将在其中打开/关闭该文件。

这带来了许多问题:

  1. 你可能会忘记打电话Release
  2. 代码没有传达有关数据流的信息r。如果r在同一范围内获取和发布,则代码不会对此进行自我记录。
  3. 在 和 之间的时间里Resource rr.Acquire(...)实际上r是可以访问的,尽管没有初始化。这是错误的来源。

应用 RAII(资源获取即初始化)方法,在 C++ 中我们获得

class ResourceRAII
{
  Resource rawResource;

  public:
  ResourceRAII(...) {rawResource = Acquire(...);}
  ~ResourceRAII() {Release(rawResource);}

  // Functions for manipulating the resource
};

...

{
  ResourceRAII r(...);

  ... Code that uses r ...
}

C++ 版本将确保你不会忘记释放资源(如果你这样做了,你就会有内存泄漏,这更容易被调试工具检测到)。它迫使程序员明确资源的数据如何流动(即:如果它只存在于函数的范围内,这将通过堆栈上的 ResourceRAII 构造来明确)。在资源对象的创建和资源无效的销毁之间没有任何意义。

它也是异常安全的!

于 2008-10-22T19:04:22.723 回答
27

宏与内联模板

C风格:

#define max(x,y) (x) > (y) ? (x) : (y)

C++ 风格

inline template<typename T>
const T& max(const T& x, const T& y)
{
   return x > y ? x : y;
}

更喜欢 C++ 方法的原因:

  • 类型安全——强制参数必须是相同的类型
  • max 定义中的语法错误将指向正确的位置,而不是您调用宏的位置
  • 可以调试进函数
于 2008-10-22T18:31:53.380 回答
18

动态数组与 STL 容器

C风格:

int **foo = new int*[n];
for (int x = 0; x < n; ++x) foo[x] = new int[m];
// (...)
for (int x = 0; x < n; ++x) delete[] foo[x];
delete[] foo;

C++ 风格:

std::vector< std::vector<int> > foo(n, std::vector<int>(m));
// (...)

为什么 STL 容器更好:

  • 它们是可调整大小的,数组具有固定大小
  • 它们是异常安全的 - 如果在 (...) 部分发生未处理的异常,则数组内存可能会泄漏 - 容器是在堆栈上创建的,因此在展开期间它将被正确销毁
  • 他们实现边界检查,例如vector::at()(超出数组范围很可能会产生访问冲突并终止程序)
  • 它们更易于使用,例如vector::clear()与手动清除数组
  • 它们隐藏了内存管理细节,使代码更具可读性
于 2008-10-22T19:27:50.587 回答
17

#define 与 const

我不断从长期编写 C 代码的开发人员那里看到这样的代码:

#define MYBUFSIZE 256

.  .  . 

char somestring[MYBUFSIZE];

等等等等

在 C++ 中,这会更好:

const int MYBUFSIZE = 256;

char somestring[MYBUFSIZE];

当然,更好的是让开发人员使用 std::string 而不是 char 数组,但这是一个单独的问题。

C 宏的问题很多——在这种情况下,没有类型检查是主要问题。

从我所见,这似乎是 C 程序员转换为 C++ 以打破的一个极其困难的习惯。

于 2008-10-22T19:32:16.037 回答
14

默认参数:

C:

void AddUser(LPCSTR lpcstrName, int iAge, const char *lpcstrAddress);
void AddUserByNameOnly(LPCSTR lpcstrName)
  {
  AddUser(lpcstrName, -1,NULL);
  }

C++ 替换/等效:

void User::Add(LPCSTR lpcstrName, int iAge=-1, const char *lpcstrAddress=NULL);

为什么是改进:

允许程序员以更少的源代码行和更紧凑的形式编写表达程序的功能。还允许将未使用参数的默认值表示为最接近它们实际使用的位置。对于调用者,简化了类/结构的接口。

于 2008-10-22T18:50:08.710 回答
13

C 的qsort函数与 C++ 的sort函数模板。后者通过具有明显和不太明显后果的模板提供类型安全:

  • 类型安全使代码不易出错。
  • 的界面sort稍微简单一些(无需指定元素的大小)。
  • 编译器知道比较器函数的类型。如果用户传递函数对象而不是函数指针,则sort执行速度会比qsort内联比较变得微不足道。对于 C 版本中必需的函数指针,情况并非如此。

以下示例演示了qsortvssort在 C 样式数组中的用法int

int pint_less_than(void const* pa, void const* pb) {
    return *static_cast<int const*>(pa) - *static_cast<int const*>(pb);
}

struct greater_than {
    bool operator ()(int a, int b) {
        return a > b;
    }
};

template <std::size_t Size>
void print(int (&arr)[Size]) {
    std::copy(arr, arr + Size, std::ostream_iterator<int>(std::cout, " "));
    std::cout << std::endl;
}

int main() {
    std::size_t const size = 5;
    int values[] = { 4, 3, 6, 8, 2 };

    { // qsort
        int arr[size];
        std::copy(values, values + size, arr);
        std::qsort(arr, size, sizeof(int), &pint_less_than);
        print(arr);
    }

    { // sort
        int arr[size];
        std::copy(values, values + size, arr);
        std::sort(arr, arr + size);
        print(arr);
    }

    { // sort with custom comparer
        int arr[size];
        std::copy(values, values + size, arr);
        std::sort(arr, arr + size, greater_than());
        print(arr);
    }
}
于 2008-10-22T20:09:31.267 回答
8

结构内联初始化与内联构造函数

有时,我们需要在 C++ 中进行简单的数据聚合。数据有些独立,通过封装来保护它是不值得的。

// C-like code in C++
struct CRect
{
   int x ;
   int y ;
} ;

void doSomething()
{
   CRect r0 ;               // uninitialized
   CRect r1 = { 25, 40 } ;  // vulnerable to some silent struct reordering,
                            // or adding a parameter
}

; 我看到上面的代码存在三个问题:

  • 如果对象没有被特别初始化,它就不会被初始化
  • 如果我们改变 x 或 y(无论出于何种原因),doSomething() 中的默认 C 初始化现在将是错误的
  • 如果我们添加 az 成员,并希望它默认为“零”,我们仍然需要更改每个内联初始化

下面的代码将内联构造函数(如果真的有用),因此成本为零(如上面的 C 代码):

// C++
struct CRect
{
   CRect() : x(0), y(0) {} ;
   CRect(int X, int Y) : x(X), y(Y) {} ;
   int x ;
   int y ;
} ;

void doSomething()
{
   CRect r0 ;
   CRect r1(25, 40) ;
}

(好处是我们可以添加一个 operator== 方法,但是这个好处超出了主题,因此值得一提但不值得作为答案。)

编辑:C99 已命名初始化

Adam Rosenfield 发表了一个有趣的评论,我觉得非常有趣:

C99 允许命名初始化器: CRect r = { .x = 25, .y = 40 }

这不会在 C++ 中编译。我想这应该被添加到 C++ 中,如果只是为了 C 兼容的话。无论如何,在 C 中,它缓解了这个答案中提到的问题。

于 2008-10-22T17:49:45.213 回答
7

iostream 与 stdio.h

在 C 中:

#include <stdio.h>

int main()
{
    int num = 42;

    printf("%s%d%c", "Hello World\n", num, '\n');

    return 0;
}

格式字符串是在运行时解析的,这意味着它不是类型安全的。

在 C++ 中:

#include <iostream>

int main()
{
    int num = 42;

    std::cout << "Hello World\n" << num << '\n';
}

数据类型在编译时是已知的,并且由于不需要格式字符串,因此需要输入的内容也更少。

于 2008-10-22T19:22:05.620 回答
5

继 fizzer 在C++ 构造替换 C 构造的帖子之后,我将在这里写下我的答案:

警告:下面提出的 C++ 解决方案不是标准 C++,而是 g++ 和 Visual C++ 的扩展,并被提议作为 C++0x 的标准(感谢Fizzer对此的评论)

请注意,Johannes Schaub - litb 的回答提供了另一种符合 C++03 标准的方法。

问题

如何提取C数组的大小?

建议的 C 解决方案

资料来源:C++ 宏什么时候有用?


#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])

与当前线程中讨论的“首选”模板解决方案不同,您可以将其用作常量表达式:

char src[23];
int dest[ARRAY_SIZE(src)];

我不同意 Fizzer,因为有一个模板化解决方案能够生成常量表达式(事实上,模板的一个非常有趣的部分是它们在编译时生成常量表达式的能力)

无论如何,ARRAY_SIZE 是一个能够提取 C 数组大小的宏。我不会详细说明 C++ 中的宏:目的是找到一个相同或更好的 C++ 解决方案。

更好的 C++ 解决方案?

以下 C++ 版本没有任何宏问题,并且可以以相同的方式执行任何操作:

template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
   // return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph's comment.
}

示范

如以下代码所示:

#include <iostream>

// C-like macro
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])

// C++ replacement
template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
   // return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph's comment.
}

int main(int argc, char **argv)
{
   char src[23];
   char * src2 = new char[23] ;
   int dest[ARRAY_SIZE(src)];
   int dest2[array_size(src)];

   std::cout << "ARRAY_SIZE(src)  : " << ARRAY_SIZE(src) << std::endl ;
   std::cout << "array_size(src)  : " << array_size(src) << std::endl ;
   std::cout << "ARRAY_SIZE(src2) : " << ARRAY_SIZE(src2) << std::endl ;
   // The next line won't compile
   //std::cout << "array_size(src2) : " << array_size(src2) << std::endl ;

   return 0;
}

这将输出:

ARRAY_SIZE(src)  : 23
array_size(src)  : 23
ARRAY_SIZE(src2) : 4

在上面的代码中,宏将指针误认为是数组,因此返回了错误的值(4,而不是 23)。相反,模板拒绝编译:

/main.cpp|539|error: no matching function for call to ‘array_size(char*&)’|

从而证明模板解决方案是: * 能够在编译时生成常量表达式 * 如果以错误的方式使用,能够停止编译

结论

因此,总而言之,模板的参数是:

  • 没有类似宏的代码污染
  • 可以隐藏在命名空间内
  • 可以防止错误的类型评估(指向内存的指针不是数组)

注意:感谢 Microsoft 为 C++ 实现 strcpy_s ...我知道这有一天会为我服务... ^_^

http://msdn.microsoft.com/en-us/library/td1esda9.aspx

编辑:该解决方案是针对 C++0x 标准化的扩展

Fizzer 确实正确地评论了这在当前的 C++ 标准中是无效的,并且是完全正确的(因为我可以在 g++ 上通过 -pedantic 选项进行验证)。

尽管如此,这不仅在今天的两个主要编译器(即 Visual C++ 和 g++)上可用,而且在 C++0x 中也考虑过,正如以下草案中所提议的那样:

C++0x 的唯一变化可能是这样的:

inline template <typename T, size_t size>
constexpr size_t array_size(T (&p)[size])
{
   //return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph's comment.
}

(注意constexpr关键字)

编辑 2

Johannes Schaub - litb 的答案提供了另一种符合 C++03 的方法。我将在此处复制粘贴源代码以供参考,但请访问他的答案以获取完整示例(并对其进行升级!):

template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];

用作:

int p[] = { 1, 2, 3, 4, 5, 6 };
int u[sizeof array_size(p)]; // we get the size (6) at compile time.

我大脑中的许多神经元都被炸了,以使我了解它的本质array_size(提示:它是一个返回对 N 字符数组的引用的函数)。

:-)

于 2008-10-22T20:25:45.273 回答
4

局部(自动)变量声明

(自 C99 以来不正确,正如 Jonathan Leffler 正确指出的那样)

在 C 中,您必须在定义它们的块的开头声明所有局部变量。

在 C++ 中,可以(并且更可取)在必须使用之前推迟变量定义。以后更可取,主要有两个原因:

  1. 它提高了程序的清晰度(当您看到第一次使用它的变量类型时)。
  2. 它使重构更容易(因为你有小块内聚的代码)。
  3. 它提高了程序效率(因为变量是在实际需要时构建的)。
于 2008-10-23T13:07:36.467 回答
4

铸造 C 方式(type)static_cast<type>(). 在stackoverflow上看到那里那里的主题

于 2008-10-22T20:25:45.337 回答
2

遵循paercebal的构造,使用可变长度数组来解决函数不能返回常量表达式的限制,这里有一种方法可以做到这一点,以某种其他方式:

template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];

我已经在我的其他一些答案中写了它,但它不适合任何地方比这个线程更好。现在,好吧,这是一个如何使用它的方法:

void pass(int *q) {
    int n1 = sizeof(q); // oops, size of the pointer!
    int n2 = sizeof array_size(q); // error! q is not an array!
}

int main() {
    int p[] = { 1, 2, 3, 4, 5, 6 };
    int u[sizeof array_size(p)]; // we get the size at compile time.

    pass(p);
}

优于 sizeof

  1. 非数组失败。不会默默地为指针工作
  2. 将在代码中告知数组大小已被采用。
于 2009-01-04T20:12:52.800 回答
2

我将提供一些可能非常明显的东西,命名空间。

c 拥挤的全局作用域:

void PrintToScreen(const char *pBuffer);
void PrintToFile(const char *pBuffer);
void PrintToSocket(const char *pBuffer);
void PrintPrettyToScreen(const char *pBuffer);

对比

c++ 对全局范围、命名空间的可定义细分:

namespace Screen
{
   void Print(const char *pBuffer);
}

namespace File
{
   void Print(const char *pBuffer);
}

namespace Socket
{
   void Print(const char *pBuffer);
}

namespace PrettyScreen
{
   void Print(const char *pBuffer);
}

这是一个人为的例子,但是将您定义的标记分类到有意义的范围内的能力可以防止函数的用途与调用它的上下文混淆。

于 2008-12-31T02:50:29.033 回答
2

作为对Alex Che的回应,并且公平地对待 C:

在 C99(C 的当前 ISO 标准规范)中,变量可以在块中的任何位置声明,与 C++ 中相同。以下代码是有效的 C99:

int main(void)
{
   for(int i = 0; i < 10; i++)
      ...

   int r = 0;
   return r;
}
于 2008-10-29T00:33:40.927 回答
1

std::copy对比memcpy

首先是可用性问题:

  • memcpy采用 void 指针。这抛出了类型安全。
  • std::copy在某些情况下允许重叠范围(std::copy_backward其他重叠情况存在),而memcpy永远不允许。
  • memcpy仅适用于指针,而std::copy适用于迭代器(其中指针是一种特殊情况,因此std::copy也适用于指针)。这意味着您可以,例如,std::copy在 a 中的元素std::list

当然,所有这些额外的安全性和通用性都是有代价的,对吧?

当我测量时,我发现它std::copymemcpy.

换句话说,似乎没有理由memcpy在真正的 C++ 代码中使用。

于 2012-05-05T17:36:03.083 回答
0

C++ 中的新功能与 C 中的 malloc。(用于内存管理)

new 运算符允许调用类构造函数,而 malloc 不允许。

于 2009-01-04T20:39:59.003 回答
0

重载函数:

C:

AddUserName(int userid, NameInfo nameinfo);
AddUserAge(int userid, int iAge);
AddUserAddress(int userid, AddressInfo addressinfo);

C++ 等效/替换:

User::AddInfo(NameInfo nameinfo);
User::AddInfo(int iAge);
User::AddInfo(AddressInfo addressInfo);

为什么是改进:

允许程序员表达接口,使得函数的概念在名称中表达,而参数类型仅在参数本身中表达。允许调用者以更接近概念表达的方式与类进行交互。通常也会产生更简洁、紧凑和可读的源代码。

于 2008-10-24T16:12:46.487 回答
0

iostreams

Formatted I/O may be faster using the C runtime. But I don't believe that low-level I/O (read,write,etc.) is any slower with streams. The ability to read or write to a stream without caring if the other end is a file, string, socket or some user-defined object is incredibly useful.

于 2008-10-24T21:16:44.673 回答
0

在 c 中,您的大部分动态功能都是通过传递函数指针来实现的。C++ 允许您拥有函数对象,从而提供更大的灵活性和安全性。我将展示一个改编自 Stephen Dewhurst 出色的C++ Common Knowledge的示例

C 函数指针:

int fibonacci() {
  static int a0 = 0, a1 =1; // problematic....
  int temp = a0;
  a0 = a1;
  a1 = temp + a0;
  return temp;
}

void Graph( (int)(*func)(void) );
void Graph2( (int)(*func1)(void), (int)(*func2)(void) ); 

Graph(fibonacci);
Graph2(fibonacci,fibonacci);

您可以看到,给定函数中的静态变量,fibonacci()执行的顺序GraphGraph2()将改变行为,尽管调用Graph2()可能会在每次调用时产生意想不到的结果,func1并且func2会产生系列中的下一个值,而不是系列的单个实例中与被调用函数相关的下一个值。(显然,您可以将函数的状态外部化,但这将失去重点,更不用说让用户感到困惑并使客户端函数复杂化)

C++ 函数对象:

class Fib {
  public:
    Fib() : a0_(1), a1_(1) {}
    int operator();
  private:
    int a0_, a1_;
};
int Fib::operator() {
    int temp = a0_;
    a0_ = a1_;
    a1_ = temp + a0_;
    return temp;
}


template <class FuncT>
void Graph( FuncT &func );

template <class FuncT>
void Graph2( FuncT &func1, FuncT &func2); 

Fib a,b,c;
Graph(a);
Graph2(b,c);

在这里,Graph()andGraph2()函数的执行顺序不会改变调用的结果。此外,在调用Graph2() bc维护单独的状态时,它们被使用;每个都将单独生成完整的斐波那契数列。

于 2008-12-31T03:23:20.107 回答
0

为了平衡起见,这篇文章有一个 C 风格构造的示例,它有时比 C++ 风格等效。

于 2008-10-22T19:57:45.360 回答
-4

几乎所有使用void*.

于 2008-10-22T18:00:39.467 回答