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>的函数来进行字符串操作。

字符数组是个麻烦东西,之前我写过一篇博客讨论数组与指针的区别。参见

数组与指针的区别,以及在STL中传递数组/指针

数组比起包装好的类,一个显著差异就是在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[] = { ,, };

但是字符数组像这样初始化太麻烦,来体会一下

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() };
strncpy(s1.s, "hello", sizeof("hello"));
String s2 = s1;
s2.s[] = '-';
puts(s1.s); // h-llo
free(s1.s);
return ;
}

注意,这里我用了动态分配,如果只是用字符串字面值的话,指针指向的区域(字符串字面值存储在常量区)是不能更改的。在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 : ,
s : "hello"
};
printf("%d %s\n", obj.i, obj.s);
    Object obj = {
.i = ,
.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++中均无法通过编译,如下图

因为我刚才提到了,那是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赖“兼容”!

C语言(C99标准)在结构体的初始化上与C++的区别的更多相关文章

  1. C语言结构体的初始化

    今天在工作时,看到了奇葩的结构体初始化方式,于是我查了一下C99标准文档和gcc的说明文档,终于搞清楚是怎么回事了. 假设有如下结构体定义: typedef struct { int a, b, c; ...

  2. OpenGL ES着色器语言之语句和结构体(官方文档第六章)内建变量(官方文档第七、八章)

    OpenGL ES着色器语言之语句和结构体(官方文档第六章) OpenGL ES着色器语言的程序块基本构成如下: 语句和声明 函数定义 选择(if-else) 迭代(for, while, do-wh ...

  3. C89,C99: C数组&结构体&联合体快速初始化

    1. 背景 C89标准规定初始化语句的元素以固定顺序出现,该顺序即待初始化数组或结构体元素的定义顺序. C99标准新增指定初始化(Designated Initializer),即可按照任意顺序对数组 ...

  4. C语言一维数组、二维数组、结构体的初始化

    C语言数组的初始化表示方法 一.C语言一维数组初始化: (1)在定义数组时对数组元素赋以初值.如: static int a[10]={0,1,2,3,4,5,6,7,8,9}; 经过上面的定义和初始 ...

  5. Android For JNI(五)——C语言多级指针,结构体,联合体,枚举,自定义类型

    Android For JNI(五)--C语言多级指针,结构体,联合体,枚举,自定义类型 我们的C已经渐渐的步入正轨了,基础过去之后,就是我们的NDK和JNI实战了 一.多级指针 指针的概念我们在前面 ...

  6. C语言入门第十章----结构体

    C语言结构体从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由int.char .float等基本类型组成的,你可以认为结构体是一种聚合类型. 在实际开发中,我们可以将一组类型不同的. ...

  7. 黑马程序员——C语言基础 变量类型 结构体

    Java培训.Android培训.iOS培训..Net培训.期待与您交流! (以下内容是对黑马苹果入学视频的个人知识点总结) (一)变量类型 1)局部变量 1> 定义:在函数内部定义的变量,称为 ...

  8. 关于c语言中结构体的初始化

    1.先定义结构体类型后再定义结构体变量: 格式为:struct 结构体名 变量名列表: struct book s1,s2,*ss://注意这种之前要先定义结构体类型后再定义变量: 2.在定义结构体类 ...

  9. (转)关于linux中内核编程中结构体的赋值操作(结构体指定初始化)

    网址:http://blog.chinaunix.net/uid-24807808-id-3219820.html 在看linux源码的时候,经常会看到类似于下面的结构体赋值的代码: struct d ...

随机推荐

  1. vue 跨域

    注意!只能在本地调试使用,上线后url会出错使用以下方法要先引入网络模块 先配置文件:config =>index.js以下部分改为:proxyTable: { '/apis': { // 测试 ...

  2. 013——数组(十三) array_push array_rand array_reverse

    <?php /* 数组 array_push array_rand array_reverse */ //array_push()在数组的末端,增加一个或多个元素,入栈 /*$array = a ...

  3. docker 快速搭建 WordPress

    安装Docker 环境:阿里云服务器 镜像:CentOs 7.4 64 https://docs.docker.com/install/linux/docker-ce/centos/ 安装WordPr ...

  4. yii2 实现excel导出功能

    官方教程地址:http://www.yiiframework.com/extension/yii2-export2excel/ 安装: Either run php composer.phar req ...

  5. 字符串比较,栈溢出引起的程序bug

    需求 输入密码字符串,与设定的密码“1234567”进行比较,两者相符则输出"congratulations!”,不符则输出“try again!”. 程序bug 实际运行过程中发现,输入某 ...

  6. L151

    In Toothy Prequel, Piranha-Like Fish Menaced Jurassic Seas You can call it a prehistoric prequel.Sci ...

  7. 疑问:@Autowired的作用?[待解答]

    有下面一个Spring的工程,工程结构如下: 代码如下: applicationContext.xml: <?xml version="1.0" encoding=" ...

  8. PostgreSQL资料汇总

    慢慢积累一些有用的资料: https://postgrespro.ru

  9. spring之httpclient doget请求

    /**     * @param url        请求地址     * @param jsonString 加密后的字符串     * @return     * @throws ClientP ...

  10. 使用pipework将Docker容器配置到本地网络环境中

    使用pipework将Docker容器配置到本地网络环境中 需求 在使用Docker的过程中,有时候我们会有将Docker容器配置到和主机同一网段的需求.要实现这个需求,我们只要将Docker容器和主 ...