C ++构造替换C构造

时间:2008-10-22 17:48:05

标签: c++ c language-construct

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

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

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

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

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

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

注3:这不是一个抨击C的问题。我想要理由。吹嘘,抨击和未经证实的比较将被下调。提及没有C等价物的C ++特性可能被认为是不可能的主题:我希望并排放置一个针对C ++特征的C特征。

21 个答案:

答案 0 :(得分: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实际上是可访问的,尽管未初始化。这是错误的来源。
  4. 应用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构造来表明)。在创建资源对象和资源无效的销毁之间没有任何意义。

    它的例外安全!

答案 1 :(得分: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定义中的语法错误将指向正确的位置,而不是您调用宏的位置
  • 可以调试功能

答案 2 :(得分: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()与手动清除阵列
  • 他们隐藏了内存管理细节,使代码更具可读性

答案 3 :(得分:17)

#define vs. const

我一直看到这样的代码来自长期编码C的开发人员:

#define MYBUFSIZE 256

.  .  . 

char somestring[MYBUFSIZE];

等。等

在C ++中,这会更好:

const int MYBUFSIZE = 256;

char somestring[MYBUFSIZE];

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

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

从我所看到的情况来看,对于转换为C ++的C程序员而言,这似乎是一个非常难的习惯。

答案 4 :(得分: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);

为什么这是一个改进:

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

答案 5 :(得分:13)

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

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

以下示例演示了qsortsort的C样式数组的intint 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); } } 的用法。

{{1}}

答案 6 :(得分:8)

struct inline initialization vs. inline constructors

有时,我们需要在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初始化现在将是错误的
  • 如果我们添加一个z成员,并且默认情况下喜欢它为“零”,我们仍然需要更改每个内联初始化

下面的代码将内联构造函数(如果真的很有用),因此将具有零成本(如上面的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) ;
}

(奖励是我们可以添加一个运算符==方法,但这个奖励超出了主题,所以值得一提,但不值得作为答案。)

编辑:C99已命名已初始化

亚当罗森菲尔德做了一个有趣的评论我觉得很有意思:

C99允许命名的初始值设定项:     CRect r = {。x = 25,.y = 40}

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

答案 7 :(得分:7)

iostream vs 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';
}

数据类型在编译时是已知的,因为不需要格式字符串,所以也可以输入更少的数据类型。

答案 8 :(得分:5)

在fizzer的帖子C++ constructs replacing C constructs之后,我会在这里写下我的回答:

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

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

问题

如何提取C数组的大小?

建议的C解决方案

来源:When are C++ macros beneficial?


#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's answer提供另一种符合C ++ 03标准的方法。我将复制粘贴源以供参考,但请访问他的答案以获得完整的示例(和upmod it!):

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个字符数组的引用的函数)。

: - )

答案 9 :(得分:4)

投射C路(type)static_cast<type>()。有关主题

,请参阅stackoverflow上的therethere

答案 10 :(得分:4)

本地(自动)变量声明

(自C99以来不正确,Jonathan Leffler正确指出)

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

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

  1. 它提高了程序的清晰度(正如您所看到的第一次使用它的变量类型)。
  2. 它使重构变得更容易(因为你有很小的内聚代码)。
  3. 它提高了程序效率(因为变量是在实际需要时构建的)。

答案 11 :(得分: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. 将在代码中告知采用数组大小​​。

答案 12 :(得分:2)

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

c拥挤的全球范围:

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

VS

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);
}

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

答案 13 :(得分:2)

回应Alex Che,并公平地对待C:

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

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

   int r = 0;
   return r;
}

答案 14 :(得分:1)

std::copymemcpy

首先是可用性问题:

  • memcpy采用无效指针。这会导致类型安全。
  • std::copy允许在某些情况下重叠范围(其他重叠案例中存在std::copy_backward),而memcpy则不允许重叠范围。
  • memcpy仅适用于指针,而std::copy适用于迭代器(其中指针是一种特殊情况,因此std::copy也适用于指针)。这意味着您可以使用std::copy。{/ li>中的std::list元素

所有这些额外的安全性和普遍性都是有代价的,对吗?

当我测量时,我发现std::copy had a slight performance advantage over memcpy

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

答案 15 :(得分:0)

<强>输入输出流

使用C运行时,格式化I / O可能会更快。但我不相信低级I / O(读,写等)在使用流时会更慢。如果另一端是文件,字符串,套接字或某些用户定义的对象,则无需关心即可读取或写入流的能力非常有用。

答案 16 :(得分: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);

为什么这是一个改进:

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

答案 17 :(得分: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()的执行顺序将改变行为,而不是调用{ {1}}可能会产生意外结果,因为每次调用Graph2()func1都会产生系列中的下一个值,而不是系列的单个实例中与被调用函数相关的下一个值。 (显然你可以将函数的状态外部化,但这将忽略这一点,更不用说让用户感到困惑并使客户端函数复杂化了)

C ++函数对象:

func2

此处,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()函数的执行顺序不会改变调用的结果。此外,在Graph2() Graph2()b的调用中,在使用时保持单独的状态;每个都将单独生成完整的斐波那契序列。

答案 18 :(得分:0)

为了平衡,this post有一个C风格结构的例子,它有时比C ++风格的等价物更好。

答案 19 :(得分:0)

C ++中的新内容与C中的malloc相比(用于内存管理)

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

答案 20 :(得分:-4)

几乎使用void*