从零开始学C++之从C到C++(一):const与#define、结构体对齐、函数重载name mangling、new/delete 等

一、bool 类型

逻辑型也称布尔型,其取值为true(逻辑真)和false(逻辑假),存储字节数在不同编译系统中可能有所不同,VC++中为1个字节。 声明方式:bool result; result=true; 可以当作整数用(true一般为1,false为0) 把其它类型的值转换为布尔值时,非零值转换为true,零值转换为false,注意会发生截断。

二、const 限定符

(1)、用const给字面常量起个名字(标识符),这个标识符就称为标识符常量;因为标识符常量的声明和使用形式很像变量,所以也称常变量。 定义的一般形式: const 数据类型 常量名=常量值; 数据类型 const 常量名=常量值; 例如: const  float  PI=3.14159f; 注意事项:

常变量在定义时必须初始化; 常变量初始化之后,不允许再被赋值;

正如我在这里所说,其实加了关键字const只是提示编译器这个变量是常量,如果我们在接下来的操作中试图更改它,编译器会报错,而并不是真正的常量,事实上某些情形下通过指针也是可以更改的(编译器报警告),什么情况下完全不能修改呢,当A是加const限定且初始化的全局变量,此时A位于.rodata段(linux 下)。此外const 用于修饰指针时可以参考这里

(2)、const 与 #define

const定义的常量与#define定义的符号常量的区别:

const定义的常量有类型,而#define定义的没有类型,编译可以对前者进行类型安全检查,而后者仅仅只是做简单替换const定义的常量在编译/运行时确定初值,而#define定义的常量是在预编译时进行替换,不分配内存。

作用域不同,const定义的常变量的作用域为该变量的作用域范围。而#define定义的常量作用域为它的定义点到程序结束,当然也可以在某个地方用#undef取消

#define定义的常量,容易产生副作用:

//Effective C++ 3rd的一个例子。 #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) int a = 5; int b = 0; CALL_WITH_MAX(++a, b); //a被累加二次 CALL_WITH_MAX(++a, b+10); //a被累加一次 在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”

此外,定义常量还可以用enum,在c++ 中尽量用const、enum替换#define定义常量,用inline 替换带参数的宏定义;但 #define 在底层编程中是必不可少的,下面举个例子:

#include<iostream>
using namespace std;

#define STR(a) #a
#define CAT(a,b) a##b

int main(void)
{
    int xy = 100;
    cout << STR(ABCD) << endl; // #ABCD => "ABCD"
    cout << CAT(x, y) << endl; // x##y => xy

    return 0;
}

如果是完全的c++ 菜鸟,这里还得稍微解释一下细节,iostream 是c++标准库的一个io流头文件,跟C语言不太一样的是一般没有.h 后缀,using namespace 表示命名空间,简单理解就是统一的函数前缀,类比pthread库如pthread_mutex_init, pthread_mutex_lock 用c++ 方式来表示可能是 pthread::mutex::lock。 cout是输出流对象,<<操作符在C语言中是左移位运算操作符,在这里被重载成输出操作符,之所以能并列输出是因为如cout<<xxx 返回的是cout 的引用,以后还会再提。参数宏定义的意义就很清楚了,查看下输出即可。

我们知道printf函数带有可变参数,函数式宏定义也可以带可变参数,同样是在参数列表中用...表示可变参数。例如:

#define showlist(...) printf(#__VA_ARGS__)
#define report(test, ...) ((test)?printf(#test):\
                                printf(__VA_ARGS__))

showlist(The first, second, and third items.);
report(x > y, "x is %d but y is %d", x, y);

预处理之后变成: 

C++ Code 

1 2

printf("The first, second, and third items."); ((x > y) ? printf("x>y") : printf("x is %d but y is %d", x, y));

在宏定义中,可变参数的部分用__VA_ARGS__表示,实参中对应...的几个参数可以看成一个参数替换到宏定义中__VA_ARGS__所在的地方。

(三)、结构体对齐,初始化方式

什么是内存对齐

编译器为每个“数据单元”按排在某个合适的位置上。

C、C++语言非常灵活,它允许你干涉“内存对齐”

为什么要对齐

性能原因:在对齐的地址上访问数据快。

如何对齐

第一个数据成员放在offset为0的位置

其它成员对齐至min(sizeof(member),#pragma pack(n)所指定的值)的整数倍。

整个结构体也要对齐,结构体总大小对齐至各个min中最大值的整数倍。

举个例子,

struct test

{

char a;

double b;

char c;

};

根据规则1,a 在位置0;根据规则2,因为VC默认pack为8,可以通过项目-》属性-》c/c++  -》代码生成-》结构体成员对齐选项修改,也可以使用#pragma pack(n) 来修改,#pragma pack() 取消修改,那么b 占据8~15;根据规则2,c在16;现在总共17个字节,根据规则3,结构体总大小需对齐到8的整数倍,即总共是24个字节。

如果将pack 修改为4,则总大小为16。在VC上pack 共有1,2,4,8,16 等5种选择,而linux g++ 则只有1,2,4 可选,默认是4。

推荐的的初始化方法应该是(当然,一个成员一个成员地初始化也行): struct s {     int   l;     char* p; };

struct s s1 = {.l=4, .p = "abcd"}; struct s s2 = {l:4, p:"abcd"}; 不建议直接写死如  struct s s1 = {4, "abcd"}; 原因在于你对结构的内存布局作了假设。如果这个结构是第三方提供的,他很可能调整结构中成员的相对位置。而这样的调整往往不会在文档中说明,你自然很少去关注。如果调整的两个成员具有相同数据类型,编译时不会有任何警告,而程序的逻辑上可能相距十万八千里了。

(四)、域运算符

C++中增加的作用域标识符 ::

用于对与局部变量同名的全局变量进行访问

用于表示类的静态成员,以后讲到类的时候再详谈

(五)、new、delete 运算符

(1)、new operator

new运算符可以用于创建堆空间,成功返回首地址,失败抛出异常 语法: 指针变量=new 数据类型(值); 指针变量=new 数据类型[长度n]; 例如: int *p; p=new int(3); char *pStr=new char[50];

(2)、delete operator

delete运算符 可以用于释放堆空间 语法: delete 指针变量; delete [] 指针变量; 例如: delete p; delete [] pStr; // 类似 delete pStr[0] , delete pStr[1],  ....

(3)、new 和 delete 执行的步骤

new operator

内存分配(operator new),类似malloc

调用构造函数,讲到类再说

delete operator

调用析构函数,讲到类再说

释放内存(operator delete),类似free

实际上new 有三种用法,包括operator new、new operator、placement new,new operator 包含operator new,而placement new 则没有内存分配而是直接调用构造函数,具体的差异以后再谈。

(六)、函数重载、name managling 与extern "C"

(1)、函数重载

相同的作用域,如果两个函数名称相同,而参数不同,我们把它们称为重载overload,函数重载又称为函数的多态性(静态) 函数重载不同形式:

形参数量不同

形参类型不同

形参的顺序不同

形参数量和形参类型都不同

调用重载函数时,编译器通过检查实际参数的个数、类型和顺序来确定相应的被调用函数。

函数的重载跟函数的覆盖、函数的隐藏是不同的,这一点以后再讲。

合法的重载例子: int  abs(int i); long abs(long l);double abs(double d); 非法的重载例子:

int  abs(int i); long abs(int i); void abs(int i); //如果返回类型不同而函数名相同、形参也相同,则是不合法的,编译器会报"语法错误"。

(2)、name mangling 与extern "C"

name managling这里把它翻译为名字改编,C++为了支持函数重载,需要将函数名根据参数的不同进行name managling以便区分。

extern “C” 可以实现C与C++混合编程,被extern "C" 修饰的变量和函数是按照C语言方式进行编译和链接的,即对C语言写的函数不进行改名,一般在

C的头文件中使用,如果头文件被C代码包含并用C编译器编译,则__cplusplus 没有定义,extern “C" 被略过,如果头文件被C++代码包含并被C++编

译器编译,存在__cplusplus 定义故extern "c" 提示编译器不要对 {} 内函数进行改名。

#ifdef __cpluscplus extern “C” { #endif ... #ifdef __cpluscplus } #endif

实际上,编译器对数据成员也会进行name mangling处理,目地是区分派生类和基类中可能的同名成员。

不同C++编译器的name mangling 方案是不同的,这是造成不同编译器之间存在二进制连接兼容性的主要原因之一。

(七)、带默认形参值的函数

函数声明或者定义的时候,可以给形参赋一些默认值,调用函数时,若没有给出实参,则按指定的默认值进行工作。

* 函数没有声明时,在函数定义中指定形参的默认值 * 函数既有定义又有声明时,声明时指定后,定义后就不能再指定默认值 * 默认值的定义必须遵守从右到左的顺序,如果某个形参没有默认值,则它左边的参数就不能有默认值。 void func1(int a, double b=4.5, int c=3); //合法  void func1(int a=1, double b, int c=3);  //不合法 * 函数调用时,实参与形参按从左到右的顺序进行匹配

* 重载的函数中如果形参带有默认值时,可能产生二义性

int add(int x = 5, int y = 6);
int add(int x = 5, int y = 6, int z = 7);
int main(void)
{
    int sum;
    sum = add(10, 20);
    return 0;
}

sum=add(10,20)语句产生二义性ambiguity,可以认为该语句是调用第一个函数,也可以是第二个,因此编译器不能确定调用的是哪一个函数。

参考:

C++ primer 第四版 Effective C++ 3rd C++编程规范

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏浪淘沙

java学习day07 常用API

2018.6.11 1.object 所有类的父类 toString 打印对象的地址值 hashCode 对象的存储位置的算法 ...

943
来自专栏郭耀华‘s Blog

Java String 类

字符串广泛应用 在Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。 ---- 创建字符串 创建字符串最简...

34512
来自专栏一个会写诗的程序员的博客

第7章 集合类第7章 集合类

在 Java 类库中有一套相当完整的容器集合类来持有对象。Kotlin没有去重复造轮子(Scala则是自己实现了一套集合类框架),而是在Java 类库的基础上进...

522
来自专栏小樱的经验随笔

【Java学习笔记之十九】super在Java继承中的用法小结

1)有人写了个很好的初始化属性的构造函数,而你仅仅想要在其中添加另一些自己新建属性的初始化,这样在一个构造函数中调用另外一个构造函数,可以避免重复的代码量,减少...

3275
来自专栏苦逼的码农

从0打卡leetcode之day11--正则表达式匹配

给定一个字符串 (s) 和一个字符模式 (p)。实现支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

802
来自专栏小狼的世界

Python 3.6学习笔记(一)

可以看到,注释以#开头,python的变量不需要任何前缀,行结束不需要结束符号,非常符合我们自然语言的阅读和书写习惯。当语句以:结尾时,缩紧的语句视为代码块。

772
来自专栏Ryan Miao

Java复习3-类的继承

本次学习面向对象设计的另外一个基本概念:继承(inheritance)。这是Java程序设计中的一项核心技术。另外,还要学习反射(reflection)的概念。

852
来自专栏还债之路

正则表达式

1.17 正则的引用 所在的位置就看左侧的"("所在的位置,在第一个就是\1,第二个就是\2,嵌套引用也是这个道理

863
来自专栏简书专栏

Python闭包函数和装饰器

1.概念:在一个外函数中定义了一个内函数,内函数运用了外函数的临时变量,并且外函数的返回值是内函数的引用 示例代码:演示函数嵌套和闭包。

794
来自专栏海天一树

小朋友学Python(11):变量类型

Python 定义了一些标准类型,用于存储各种类型的数据。 Python有五个标准的数据类型: Numbers(数字) String(字符串) List(列表)...

40314

扫码关注云+社区