跳到主要内容

声明和初始化

声明就是告诉计算机(内存), 我需要存放什么类型的数据, 给我一个"地儿"1.

一个例子

假设你是厨师. 你做不同的菜会用到不同的容器. 比如米饭要用饭碗, 菜品要用菜碟, 红烧鱼要用鱼盘, 铁锅炖要用大铁锅. 你要做饭, 首先得有一个容器来装饭. 而声明的过程, 就是你向你的助手要一个相应的盘子的过程.

而初始化, 就是容器内放实际值的过程. 当你拿到盘子之后, 你就可以选择做什么菜, 然后放进去. 就算同一大小, 不同的碟子盛的菜大概不会相同. 这就是初始化和赋值的过程.

int x; // 声明,x是碟子的名称(至少在使用它的块空间中, x可以作为标识这一个碟子的id)
x = 42; // 赋值的过程, 就是往里面装填的过程, 你把鱼香肉丝放了进去. 这个过程你在使用x这个盘子.

auto x = 2; //没有特殊理由需要显式指定数据类型,可以使用auto关键字. 好像你把菜炒好了, 让你的助手根据菜自己决定找个盘子装

const

  1. 修饰变量: 说明该变量不能修改

  2. 修饰指针: 指向常量的指针和指针常量

  3. 常量引用: 用于形参类型, 避免了拷贝, 又避免了函数对值的修改

  4. 修饰成员函数: 该成员函数内不能修改成员变量.

  5. const成员函数 只是告诉编译器,表明不修改类对象. 但是并不能阻止程序员可能做到的所有修改动作,比如对指针的修改,编译器可能无法检测到

  6. 类体外定义的const成员函数,在定义和声明处都需要const修饰符

  7. const对象为常量对象,它只能调用声明为const的成员函数。但是构造函数和析构函数是唯一不是const成员函数却可以被const对象调用的成员函数。 当然,一般对象当然也可以调用const成员了

  8. 为了允许修改一个类的成员变量,即使是一个const对象的数据成员,于是引入了mutable. 通过将数据成员声明为mutable, 表明此成员总是可以被更新,即使在一个const成员函数中。

// 类
class A
{
private:
const int a; // 常对象成员,只能在初始化列表赋值

public:
// 构造函数
A() { };
A(int x) : a(x) { }; // 初始化列表

// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
int &operator[](int i){
//...
};
};

void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数、更新常成员变量
const A *p = &a; // 常指针
const A &q = a; // 常引用

const A b;
b[2]; //err; coz [] not const, but b is const;

// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量
char* const p3 = greeting; // 常指针,指向字符数组变量
const char* const p4 = greeting; // 常指针,指向字符数组常量
}

// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常指针
void function4(const int& Var); // 引用参数在函数内为常量

// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();

static

static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间。

备注

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

static还有一个作用,它会把变量的可见范围限制在编译单元中,使它成为一个内部连接,这时,它的反义词为”extern”.

static作用分析总结:static总是使得变量或对象的存储形式变成静态存储,连接方式变成内部连接,对于局部变量(已经是内部连接了),它仅改变其存储方式;对于全局变量(已经是静态存储了),它仅改变其连接类型。

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前(进程)就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。

  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命令函数重名,可以将函数定位为 static。

与全局变量相比的优势
  1. 静态数据成员仍然是在类域名字空间,没用进入程序的全局名字空间,因此不存在与程序中其他全局名字冲突的可能
  2. 可以实现信息隐藏,静态数据成员可以是private成员,全局变量不行

静态数据成员必须在类定义外初始化,但const静态数据成员除外,const静态数据成员可以在类体中初始化

class Item {
private:
static int num; //不能在类体内初始化,静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
const int count; //也不能初始化
static const string name = "phone"; // 静态整型常量成员可以在类内初始化,但是 static const float phone 就不行了
};
int Item::num = 0; //类外初始化,不必再加static关键字

类中的static成员

出现的原因
  1. 需要在一个类的各个对象间交互,即需要一个数据对象为整个类而非某个对象服务。
  2. 同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。 类的static成员满足了上述的要求,因为它具有如下特征:有独立的存储区,属于整个类。
  1. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。

  2. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

类成员函数(无论是static成员函数或非static成员函数)都可以直接访问static数据成员,访问方式有两种:object.static_var 或 classname::static_var

static数据成员的类型可以是其所属类,而非static数据成员类型只允许为该类的指针或引用

class Item{
private:
static Item mem1;//ok
Item *mem2;//ok
Item mem3; //error
};

静态数据成员可以作为类成员函数的缺省参数,而非static成员不能。 分析:静态成员函数如果未初始化,系统自动会给它初始化为某个缺省值(如int初始化为0, 指针初始化为NULL等)。 所以一旦定义,其值已确定,可以作为类成员函数的缺省参数

static成员函数的引入: 如果某成员函数只访问static数据成员,而不访问任何其他的数据成员(非static),那么此成员函数就与哪个对象来调用无关。

  1. 只在类体的函数声明前加static, 类体外函数定义不能指定关键字static
  2. 静态成员函数没有this指针
  3. 访问方式也有两种

声明 declaration

一个声明就是一条语句.为程序引入一个实体,并为该实体指明类型:

  1. 类型type:定义了一组可能的值及一组(对象上的)操作
  2. 对象object:存放某种类型值的内存空间
  3. 值value:二进制位,具体的含义由其类型决定
  4. 变量variable:命名的对象

声明范围

还是那个例子

假如你是厨师, 你的顾客要求打包, 不在你的餐厅就餐. 怎么办?

此时你需要去声明一个可以外带的餐盒.

编译程序时,每个 .cpp 文件都会独立编译为一个编译单元。 编译器不知道在其他编译单元中声明了哪些名称。 这意味着,如果你定义类、函数或全局变量,则必须在使用它的每个附加 .cpp 文件中提供对它的声明。 在所有文件中,对它的每个声明必须完全相同。 当链接器尝试将所有编译单元合并成单个程序时,出现轻微的不一致会导致错误或意外行为。

为了最大程度地减少出错的可能性,C++ 采用了使用头文件来包含声明的约定。 在一个头文件中进行声明,然后在每个 .cpp 文件或其他需要该声明的头文件中使用 #include 指令。 #include 指令在编译之前将头文件的副本直接插入 .cpp 文件中。

生命周期

声明语句将一个名字引入到了一个作用域中:

  1. 局部声明: 界限是{}. 函数的参数也属于局部声明.
  2. 类作用域: 变量在一个类中声明(不在类的成员函数中声明). 其作用域和类的实例的作用域相同.
  3. 命名空间作用域: 变量声明在一个命名空间内(位于任何函数,lambda,类,enum class之外). 作用域从其声明位置开始,到名字空间结束为止.
  4. 声明在所有结构之外的名字称为全局名字,称为它位于在全局名字空间中.

此外对象也可以没有名字.(lambda)

分配

备注

在 C语言中,动态分配和释放内存的函数是 malloccallocfree,而在 C++语言中,newnew[]deletedelete[] 操作符通常会被用来动态地分配内存和释放内存。需要注意的是,newnew[]deletedelete[] 是操作符,而非函数。 newdelete 是 C++ 的关键字。

  1. new 用于动态分配单个空间
  2. new[] 则是用于动态分配一个数组
  3. delete 用于释放由 new 分配的空间
  4. delete[] 则用于释放 new[] 分配的一个数组。
Hero *p = new Hero;  //为 p 指针分配了一个 Hero 型的空间。new 操作符根据请求分配的数据类型来推断所需的空间大小。
Hero *A = new Hero[10]; // 分配了10个连续的Hero地址, A指向首地址;

为了避免内存泄露, newnew[]deletedelete[] 都是成对出现.

初始化

初始化根本上是一个表达式和语句.

初始化:在使用对象之前,必须给它赋一个值. 很简单的理解, 要吃饭, 首先得有饭. 从这个角度理解对象: 初始化后的变量.

double d1 = 1.0;
double d2 {1.0};
double d3 = {1.0};
complex<double> z = 1;
vector<int> v {1,2,3,4,5,6};
为什么使用double d2 {1.0}; 这种方式去初始化?

{}可以避免在类型转换中丢失原有信息,即避免收缩转换.

用户定义字面量

对于内置类型,我们可以声明字面值:

int x = 123;
unsigned int y = 0xFF00u;
double z = 123.456;
const char[10] = "Surprise!";

#<string>
#<chrono>
#<complex>

std::string x = "Surprise!"s;
second y = 1.2s;
complex t = 12.5i + 1.2;

Footnotes

  1. 如何理解"地儿", 丁垚老师在中国建筑史上讲过, 有兴趣可以去上网搜一下, 也许会有.

Loading Comments...