C语⾔(C99标准)在结构体的初始化上与C++的区别
C++中由于有构造函数的概念,所以很多时候初始化⼯作能够很⽅便地进⾏,⽽且由于C++标准库中有很多实⽤类(往往是类模板),现代C++能⼗分容易地编写。
⽐如现在要构造⼀个类Object,包含两个字段,⼀个为整型,⼀个为字符串。C++的做法会像下⾯这样
#include <stdio.h>
#include <string>
struct Object
{
int i;
std::string s;
Object(int _i, const char* _s) : i(_i), s(_s) { }
};
int main()
{
Object obj(1, "hello");
printf("%d %s\n", obj.i, obj.s.c_str());
return 0;
}
这样的代码简洁、安全,C++通过析构函数来实现资源的安全释放,string的c_str()⽅法能够返回const char*,⽽这个字符串指针可能指向⼀⽚在堆上动态分配的内存,string的析构函数能够保证string对象脱离作⽤域被销毁时,这段内存被系统回收。
string真正实现较为复杂,它本⾝其实是类模板basic_string的实例化,⽽且basic_string⾥⾯的类型都是⽤type_traits来进⾏类型计算得到的类型别名,通过模板参数CharT(字符类型)不同,相应的值也不同,但都是通过模板的⼿法在编译期就计算出来。⽐如字符类型CharT可以是char、char16_t、char32_t、wchar_t,对应的类模板实例化为string、u16string、u32string、wstring,共享类模板basic_string的成员函数来进⾏字符串操作。
string内部的优化措施也不同,像VS2015的basic_string就是采⽤字符串较短时c_str()指向栈上的字符数组、较长则动态分配的策略。其他系统有的可能采⽤写时复制技术,总之,⼀般⽽⾔string不会成为性能的瓶颈,符合C++既保证代码简洁⼜保证抽象带来的效率丢失尽可能⼩的设计要求。
对于C⽽⾔,就没有C++那么⽅便了。C⼀般是直接⽤字符数组来表⽰字符串,再⽤头⽂件<string.h>的函数来进⾏字符串操作。
字符数组是个⿇烦东西,之前我写过⼀篇博客讨论数组与指针的区别。参见
数组⽐起包装好的类,⼀个显著差异就是在C/C++赋值符号“=”的使⽤上。参见下⾯代码
std::string s1 = "hello";
std::string s2;
s2 = s1; // OK! 调⽤成员函数operator=
char s11[100] = "hello";
char s22[100];
/
/ s22 = s11; // Error! 数组不能作为左值!
strcpy(s22, s11); // OK! 调⽤C库函数, 但实际中最好⽤strncpy来代替strcpy防⽌溢出
不过从上⾯代码中也可以看出来C在语法上为字符数组提供了“特权”。正常来说数组可以⽤初始化列表(即⽤⼤括号括起来的若⼲元素)初始化
int a[] = { 1,2,3 };
但是字符数组像这样初始化太⿇烦,来体会⼀下
char s[] = { 'h', 'e', 'l', 'l', 'o' };
所以C可以直接⽤字符串字⾯值(string literal)来直接初始化字符数组
char s[] = "hello";
⾼下⽴判。(别看现在C语⾔的语法看起来这么原始,但其实C可是有不少“语法糖”的!)
不过这种做法仅限于初始化,在C/C++中必须得严格区分初始化和赋值,前者是给对象⼀个初始值,后者是对象已经有⼀个初始值,然后赋予⼀个新值。
再看看下⾯这份代码
std::string s1 = "hello"; // 默认构造
auto s2 = s1; // 拷贝构造
s1 = s2; // 调⽤成员函数operator =
char s11[] = "hello"; // ⽤字符串字⾯值来初始化字符数组
// char s22[] = s11; // Error! 数组只能以初始化列表或字符串字⾯值来初始化
// s22 = s11; // Error! 数组不能作为左值
但是C语⾔的结构体,对应C++的聚合类,跟普通类有所区别(具体参考C++ Primer 7.5.5),对“=”的⽀持就好得多
PS:聚合类属于POD(Plain Old Data),之前看《STL源码剖析》时对这个概念也是⼀知半解,包括后⾯针对trivial和non-trivial的模板偏特化。
#include <stdio.h>
typedef struct String
{
char s[100];
} String;
int main()
{
String s1 = { { "hello" } };
String s2 = s1;
puts(s2.s); // hello
s2.s[1] = '-';
s1 = s2;
puts(s1.s); // h-llo
return 0;
}
代码⽅⾯注意main()函数第⼀⾏我⽤了两层{},外层是⽤初始化列表初始化结构体,内层是⽤字符串字⾯值初始化数组。
两处输出的结果和预期的⼀样,但是C语⾔没有拷贝构造和运算符重载的概念啊,它是怎么做到的呢?
原因是C的赋值运算符就包含浅复制的特性,也就是说对于结构体⽽⾔,赋值操作会把等号右边的变量的每⼀位给拷贝过去。如果结构体内包含的不是字符数组⽽是字符指针,那么仅仅是复制了地址,指向的都是内存上同⼀块地址。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct String
{
char* s;
} String;
int main()
{
String s1 = { (char*)malloc(100) };
strncpy(s1.s, "hello", sizeof("hello"));
String s2 = s1;
s2.s[1] = '-';
puts(s1.s); // h-llo
free(s1.s);
return0;
}
注意,这⾥我⽤了动态分配,如果只是⽤字符串字⾯值的话,指针指向的区域(字符串字⾯值存储在常量区)是不能更改的。在C++11中,只能⽤const char*指向字符串字⾯值,因为⽤char*指向它会有错误的语义,让⽤户以为这⾥指向的字符串可以修改。
从上⾯的例⼦可以看出,即使在所谓⾯向过程的C,⽤结构体这东西把变量包装⼀下也能起到很好的作⽤,那么问题来了,回到最初的问题,⽤C语⾔实现最初的C++代码⼀样的功能该怎么去做呢?
于是C的“语法糖”⼜来了,C的结构体也⽀持初始化列表,因此可以像下⾯这样
#include <stdio.h>
typedef struct Object
{
int i;
char s[100];
} Object;
int main()
{
Object obj = { 1, "hello" };
printf("%d %s\n", obj.i, obj.s);
return 0;
}
虽然Object占⽤空间很⼤(因为要保存字符数组缓存⾜够⼤),并且对于真正较⼤的字符串这个结构体还是⽆⽤,只能动态分配。但是就现在要求实现的功能⽽⾔,这种做法是可⾏的,⽽且更为简洁。(当然,C++⽤cout会更简洁,不需要调⽤string::c_str()来取得const char*,但是我并不喜欢C++的I/
O,先不说效率,就格式化输出⽽⾔远不如printf系列简单,⽽且iostream默认与cstdio同步,导致速度很慢,关闭同
步的话使⽤iostream和cstdio可能会出问题,⼆选⼀我当然选后者,虽然平常简单测试的话混合⽤⽤也没什么)
再提⼀下,之前说过这种类型在C++⾥属于聚合类,也可以像C⼀样⽤初始化列表进⾏初始化。
到此为⽌,C的代码直接原封不动⽤C++的编译⽅式是可以通过并运⾏的。
但是毕竟C的结构体不如C++的类⽅便,⽐如我现在只想初始化字符串,在C++⾥可以重载构造函数为Object(const char*)来解决,⽽C的初始化列表必须对结构体的所有变量依次初始化。对于早期C89标准,GNU提供了这两种⽅便的初始化⽅式作为扩展
Object obj = {
i : 1,
s : "hello"
};
printf("%d %s\n", obj.i, obj.s);
Object obj = {
.i = 1,
.s = "hello"
};
printf("%d %s\n", obj.i, obj.s)
厉害了我的C,有了如此便捷且美观的初始化⽅式,就不需要像C++⼀样进⾏多种重载了。类成员变量过多的话,C++要实现灵活的初始化还是挺⿇烦的。
假如对包含3个变量(x,y,z)的类,要实现对任意(0或1或2或3)个变量初始化,C++⼀共要对构造函数重载3^2=9次。⽽且假如3个变量都是int的话,初始化x和y以及初始化y和z的构造函数就⽆法区分了。
然并卵,实际应⽤哪会出现如此蛋疼的需求,就算有,也应该把多个变量个放进⼀个类⾥形成聚合类,⼀个良好的设计⼏乎不会出现这种顾虑。
c++string类型
然⽽,这两种⽅式在C++中均⽆法通过编译,如下图
因为我刚才提到了,那是GNU的扩展,并不属于标准C。(虽然gcc编译选项⽤-std=c89或-ansi也通过了编译?)
但是,较新的C99标准⽀持了第⼆种做法,也就是可以写出像下⾯这样的代码
struct sockaddr_in srvAddr = {
.sin_family = AF_INET,
.
sin_port = htons(PORT), // PORT为⾃定义的宏,不再赘述
.sin_addr.s_addr = INADDR_ANY
};
⽽如果是C++裸写socket的话还得额外⽤个类来封装下(像MFC就提供了CAsyncSocket),或者像这样⽤旧式C风格的初始化⽅式
struct sockaddr_in srvAddr;
srvAddr.sin_family = AF_INET;
srvAddr.sin_port = htons(PORT);
srvAddr.sin_addr.s_addr = INADDR_ANY;
即使这东西进了C99标准,还是不被C++⽀持。毕竟C++有构造函数,没必要⽀持这种初始化⽅式,⽽且这⾥⽤列表初始化更简单
struct sockaddr_in srvAddr = { AF_INET, htons(PORT), INADDR_ANY };
但这样必须遵从变量在结构体中的顺序,⽐如AF_INET和htons(PORT)顺序反了的话虽然编译会通过,但是运⾏就会出问题。⽽且这种代码可读性不好,不如⽼⽼实实⽤上⾯那种。
其实写这篇博客主要是因为同学问了我⼀个类内联合体初始化的问题,当时我认为是不能⽤字符串字⾯值来对字符数组赋值,后来发现是初
始化,于是隐隐约约觉得不对,后来发现实际上是可以⽤来初始化的,只不过这种⽅式C++不⽀持导致编译⼀直没通过。(= =b)
C的做法类似这样
#include <stdio.h>
struct Object
{
int i;
char s[100];
union {
int i;
char s[100];
} u;
};
int main()
{
struct Object obj = {
.s = "hello",
.u = { .s = "world" }
};
printf("%s %s\n", obj.s, obj.u.s);
return 0;
}
C++要做到同样功能也可以,因为union也跟struct⼀样,可以使⽤构造函数,不过对于类内的union必须显式加析构函数。
这点之前我也纠结了半天,后来翻阅了C++ Primer,发现第19.6章有所提及。引⽤原⽂:
“如果union含有类类型的成员,并且该类型⾃定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的”
Primer也提到了早期C++标准是不允许union内部定义含有默认构造函数或拷贝控制成员的类,C++11标准取消了这个限制但是会把析构函数声明为deleted(说⽩了就是要你写析构函数,防⽌内存泄露,我这⾥使⽤标准库的类所以不需要在析构函数⾥添加多余释放内存的代码)
#include <iostream>
#include <string>
struct Object
{
int i;
std::string s;
union {
int i;
std::string s;
U(const char* _s) : s(_s) { }
~U() { }
} u;
Object(const char* s1, const char* s2) : s(s1), u(s2) { }
};
int main()
{
Object obj("hello", "world");
std::cout << obj.s + " " + obj.u.s << std::endl;
return 0;
}
C的union⼤多时候起到⼀种隐式类型转换的作⽤(&取地址,然后对指针类型进⾏强制转换,然后*解引⽤)来实现C风格的多态,对于
C++来说继承、模板已经可以更优雅地实现这种功能,union的作⽤也就是节省空间了。
说到底都TM赖“兼容”!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论