const 限定符

const

  • 用 const 给字面常量起个名字,这个标识符就称为标识符常量,也成为常变量
  • 定义的一般形式:
    • const 数据类型 常量名 = 常量值
    • 数据类型 const 常量名 = 常量值
  • 注意事项:
    • 常变量在定义时必须初始化
    • 常变量在初始化之后,不允许再被赋值
#include <iostream>
using namespace std;
int main(void)
{
// const int a; // Error, 常量必须初始化
const int a = 100;
// a = 200; // Error 常量不能重新被赋值

int b = 22;
const int *p; // const 在 * 左边,表示 *p 是一个常量,经由 *p 不能更改指针所指向的内容
p = &b;
// *p = 200; // Error, 常量不能被重新赋值
// int * const p2; // Error, p2 为常量 常量必须初始化
int * const p2 = &b; //const 在 * 右边,表示 p2 为常量
// int c = 100;
// p2 = &c; // Error 常量不能重新被赋值

*p2 = 200;

cout << "Hello, World!" << endl;
cout << b << endl;
return 0;
}

const 与 #define

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

  • const 定义的常量有类型,而 #define 定义的没有类型,编译可以对前者进行类型安全检查,而后者仅仅只是做简单替换
  • const 定义的常量在编译时分配内存,而 #define 定义的常量在预编译时进行替换,不分配内存
  • 作用域不同,const 定义的常变量的作用域为该变量的作用域范围,而 #define 定义的常量作用域为它的定义点到程序结束,可以在某个地方用 #undef 取消
  • 定义常量还可以用 enum, 尽量用const enum 替换 #define 定义常量,inline 可以替换宏定义
#define STR(a) #a
#define CAT(a, b) a##b
int xy = 100;
cout << STR(ABCD) << endl; //#ABCD <=> "ABCD"
cout << CAT(x, y) << endl; //x##y <=> xy

结构体、域位运算、重载

结构体内存对齐

  • 什么是内存对齐
    • 编译器为每个数据单元安排在某个合适的位置上
    • c 、c++ 语言非常灵活,它允许干涉“内存对齐”
  • 为什么要对齐
    • 性能原因:在对齐的地址上访问数据块
  • 如何对齐
    • 第一个数据成员放在 offset 为 0 的位置
    • 其他成员对齐至 min(sizeof(member), #pragma pack 所指定的值)的整数倍
    • 整个结构体也要对齐,结构体总大小对齐至各个成员中最大对齐数的整数倍

域运算符

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

  • 用于对与局部变量同名的全局变量进行访问
  • 用于表示类的成员,将在类的一节中详细说明

new delete 运算符

  • new 运算符可以用于创建堆空间
  • 成功返回首地址
  • 语法:
    • 指针变量 = new 数据类型
    • 指针变量 = new 数据类型[长度n]
  • 例如
    int *p; p = new int;
    char *pStr = new char[50];
  • new 一个新对象
    • 内存分配
    • 调用构造函数
  • new operator 内存分配 + 调用构造函数
  • operator new 只分配内存
  • placement new 不分配内存,调用拷贝构造函数
  • delete运算符 用于释放堆空间
  • 语法:
    • delete 指针变量;
    • delete[] 指针变量;
    • delete p;
    • delete[] pStr;
  • delete 释放一个对象
    • 调用析构函数
    • 释放内存(operator delete)

重载

  • 相同的作用域,如果两个函数名称相同,而参数不同,称为重载 overload
  • 函数重载又称为函数的多态性 (静态的多态)
  • 函数重载不同形式:
    • 形参数量不同
    • 形参类型不同
    • 形参的顺序不同
    • 形参数量和类型都不同
  • 调用重载函数时,编译器通过检查实际参数的个数、类型和顺序来确定相应的被调用函数。
  • 如果返回类型不同而函数名相同、形参也相同,则是非法的,会报错
  • c++ 为了支持重载,需要进行 name managling 名字改编
  • extern “C” 实现 C 与 C++ 的混合编程
#ifdef __cplusplus             //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"{

#endif

/*…*/

#ifdef __cplusplus

}

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

带默认形参值的函数

  • 函数在声明或定义时,可以给形参赋一些默认值

  • 调用函数时,若没有给出实参,则按指定的默认值进行工作

  • 函数没有声明时,在函数定义中指定形参的默认值

  • 函数既有定义又有声明时,声明时制定后,定义后不能再指定默认值

  • 默认值的定义必须遵守从右到左的顺序,如果某个形参没有默认值,则它左边的参数就不能有默认值

    • void func1(int a, double b = 4.5, int c = 3); //合法
    • void func1(int a = 1, double b, int c = 3); //不合法
  • 函数调用时,实参与形参按从左到右的顺序进行匹配

带默认形参值的函数的二义性

  • 重载的函数中如果形参中带有默认值,可能产生二义性
  • sum = add(10,20)语句产生二义性,可以认为该语句是调用第一个函数,也可以是第二个,因此编译器不能确定是哪一个函数。
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)语句产生二义性,可以认为该语句是调用第一个函数,也可以是第二个,因此编译器不能确定是哪一个函数。

}

引用

引用

引用不是变量,仅仅是变量的别名,引用没有自己独立的空间,要与它所引用的变量共享空间,对引用所做的改变其实是对它所引用的变量的改变。

  • 引用是给一个变量起别名
  • 定义引用的一般格式:
    • 类型 &引用名 = 变量名;
    • int a = 1; int &b = a; // b 是 a 的别名,因此 a 和 b 是同一个单元
  • 定义引用时一定要初始化,指明该引用变量是谁的别名
  • 在实际应用中,引用一般用作参数传递与返回值
  • 引用一经初始化,不能重新指向其他变量

const 引用

const 引用是指向 const 对象的引用

const int ival = 1024;
const int& refVal = ival; //OK, 引用和常量都是 const
int& ref2 = ival; //Error, 非 const 引用引用一个 const 对象
#include <iostream>
using namespace std;
int main() {

const int val = 1024;
const int& refVal = val;
// int& ref2 = val; // Error, nonconst reference to const object
// refVal = 200; // Error, refVal 是一个常量

int val2 = 1024;
const int& ref3 = val2; // const reference to nonconst object
double val3 = 3.14;
const int& ref4 = val3; // int temp = val3;
// const int& ref4 = tmp;
cout << ref4 << endl;
cout << val3 << endl;

// int& ref5 = val3; //Error

return 0;
}

按引用传递

参数传递:值传递和指针传递

  • 值传递 形参不能更改实参
  • 指针传递 形参可以改变实参

按引用传递

  • 引用传递方式是在函数定义时在形参前面加上引用符号&,swap(int &a, int &b)
  • 按值传递方式容易理解,但形参值的改变不能对实参产生影响
  • 地址传递方式通过形参的改变使相应的实参改变,但程序容易引起错误且难以阅读
  • 引用作为参数对形参的任何操作都能改变相应的实参的数据,又使函数调用显得方便、自然。
#include <iostream>
using namespace std;
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 5;
int b = 6;

swap(a,b); // 在函数调用时, 引用被初始化 x = a, y = b;
cout << "a = " << a << ",b = " << b << endl;
return 0;
}

引用作为返回值

  • 引用的另一个作用是用于返回引用的函数
  • 函数返回引用的一个主要目的是可以将函数放在赋值运算符的左边
  • 注意:不能返回对局部变量的引用
  • 返回引用:当函数返回引用类型的时候,没有复制返回值,而是返回对象的引用(即对象本身)。
#include <iostream>
using namespace std;
// 引用作为函数返回值
int a[] = {0,1,2,3,4};

int& index(int i)
{
return a[i]; // 返回第 i 个元素的引用,引用在函数返回的时候初始化
}

int main()
{
index(3) = 100; // 引用作为函数返回值,使得函数可以放在赋值运算符左边
// 函数返回引用,引用在函数返回的时候初始化
// index(3)是一个引用, 在函数返回的时候被初始化为 a[3]
cout << "a[3] = " << a[3] << endl;
return 0;
}

不能返回对局部变量的引用

#include <iostream>
using namespace std;
// 引用作为函数返回值
int& add(int a, int b)
{
int sum;
sum = a + b;
return sum;
}

int main(void)
{
int n = add(3, 4);
int& n2 = add(3, 4); // n2 是引用,没有自己独立的空间
// n2 的值依赖于它所引用的变量
// 如果 n2 所引用的变量的生命期结束了,也就是 n2
// 是一个无效的引用, n2 的值是不确定的

// cout << "n2 = " << n2 << endl;
cout << "n = " << n << endl;
cout << "n2 = " << n2 << endl;
return 0;
}

引用与指针区别

  • 引用访问一个变量是直接访问,而指针是间接访问
  • 引用是一个变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间
  • 引用一经初始化不能再引用其他变量,而指针可以
  • 尽可能使用引用, 不得已时使用指针

参数传递:

  • 值传递 实参要初始化形参参数要分配空间,将实参内容拷贝到形参
  • 引用传递 实参初始化时不分配空间
  • 指针传递 本质值传递,如果我们要修改指针的地址,单纯使用指针传递也是不行的。

内联函数

内联函数

  • 当程序执行函数调用时,系统要建立栈空间,保护现场,传递参数以及控制程序执行的转移等等,这些工作需要系统时间和空间的开销。有些情况下,函数本身功能简单,代码很短,但使用频率很高,程序频繁调用该函数所花时间却很多,从而降低程序执行效率
  • 为了提高效率,一个解决办法就是不使用函数,直接将函数的代码嵌入到程序中,但也有缺点:一个相同的代码重复书写,而是程序的可读性没有使用函数好
  • 为了协调效率和可读性之间的矛盾,C++ 提供了另一种方法,即定义内联函数,方法是定义函数时用修饰词 inline,内联函数在编译的时候是展开的,不涉及函数调用
inline int max(int a, int b)
{
return a > b ? a: b;
}
#define MAX(a, b) (a) > (b) ? (a) : (b)

内联函数与带参数宏区别

  • 内联函数调用时,要求实参和形参的类型一致,另外内联函数会先对实参表达式进行求值,然后传递给形参;而宏调用时只是用实参简单的替换形参
  • 内联函数是在编译的时候、在调用的地方将代码展开的,而宏是在预处理时进行替换的
  • 在 C++ 中建议采用 inline 函数来替换带参数的宏
  • 在 C++ 高层次编程 推荐用 const enum inline 替换宏,在低层次编程中,宏很灵活

新的类型转换运算符

  • 旧式转型:
    • (T)expr
    • T(expr)
  • 新式转换
    • const_cast<T>(expr)
    • static_cast<T>(expr)
    • reinterpret_cast<T>(expr)
    • dynamic_cast<T>(expr) 执行”安全向下“转型操作,也就是说支持运行时识别指针或所指向的对象,这是唯一一个无法用旧式语来进行的转型操作

const_cast

  • 用来移除对象的常量性(cast away the constness)
  • const_cast 一般用于指针或引用,不能用于对象
  • 使用 const_cast 去除 const 限定的目的不是为了修改它的内容
  • 使用 const_cast 去除 const 限定,通常是为了函数能够接受这个实际参数
#include <iostream>
using namespace std;
void fun(int &val)
{
cout << "fun = " << val << endl;
}
int main(void)
{
const int val = 100;
// int n = const_cast<int>(val); error
int n = val;

// int* p = &val; error, 无法从 const int* 转换为 int*
int* p = const_cast<int*>(&val);
*p = 200;
cout << &val << endl;
cout << p << endl;
cout << "val = " << val << endl;

const int val2 = 200;
// int& refval2 = val2;
int& refval2 = const_cast<int&>(val2);
refval2 = 300;
cout << "val2 = " << val2 << endl;
// fun(val2); // error
fun(const_cast<int&>(val2));
return 0;
}

static_cast

  • 编译器隐式执行的任何类型转换都可以由 static_cast 来完成,隐式转换是编译器自动执行的
  • 当一个较大的算术类型赋值给较小的类型时,可以用 static_cast 进行强制转换
  • 可以将 void* 指针转为某一类型指针
  • 可以将基类指针转为派生类指针
  • 无法将 const 转为为 nonconst,只能使用 const_cast
int n = static_cast<int>(3.14);
cout << "n = " << n << endl;
void *p = &n;
int *p2 = static_cast<int*>(p);

reinterpret_cast

  • reinterpret_cast 通常为操作数的位模式提供较底层的重新解释,也就是说将数据以二进制存在形式的重新解释
int i;
char *p = "This is a example";
i = reinterpret_cast<int>(p);
//此时结果,i与p的值完全相同的

int *ip;
char *pc = reinterpret_cast<char*>(ip);
//程序员需要记得pc所指向的真是对象是 int 型,并非字符串
//如果将pc当作字符串指针进行操作,可能会造成运行时错误
//如 int len = strlen(pc);

转型规则

  • 尽量避免使用强制类型转换
  • 若无法避免,尽量使用新式的类型转换