第2章、变量和基本类型
2.1、基本内置
包括算术类型+空类型(void)
2.1.1算术类型 | ||||||||||||||||||||||||
分类:整型(字符、整型数、布尔变量),浮点型
一个char大小和一个机器字节一样 C++规定:一个int至少和一个short一样,一个long至少和一个int float通常1个字(32比特),double通常2个字(64比特) 带符号类型和无符号类型 8比特的signed char:-128~127(-2^7~2^7-1);unsigned char:0~255(0~2^8-1) | ||||||||||||||||||||||||
2.1.2类型转换 | ||||||||||||||||||||||||
1、将一个负数赋给一个无符号类型,结果是初始值对无符号类型表示的总数取模后的余数 eg:unsigned char c = -1; c为255;
计算机中二进制数是以补码保存的,-1原码是10000001,反码11111110,补码11111111,看做有符号类型,就是255 若111是补码,先-1,得到反码110,再取反,101 规则: 原码到反码:符号位不变,数值位“按位取反”; 反码到补码:+1
当表达式中既有带符号类型又有无符号类型, 当带符号类型取值为负时会出现异常,因为带符号数会自动转换成无符号数 | ||||||||||||||||||||||||
2.1.3字面值常量 | ||||||||||||||||||||||||
整型和浮点型字面值 十进制:20; 八进制:024 十六进制:0x14 字符和字符串字面值 char字面值常量:单括号 字符串字面值常量:双括号,实际上是由常量字符组成的数组,编译器在字符串的结尾处添加一个空字符(‘\0’) 'a'一个字符;"A":字符的数组,两个字符 |
2.2 变量
2.2.1 变量定义 | |||
初始化方式:
默认初始化:
(1)内置类型变量如果没有被显式初始化,值由定义的位置决定。定义于任何函数体之外的变量被初始化为0;定义在函数体内部的内置变量不被初始化,一个不被初始化的内置类型变量的值是未定义的,如果试图拷贝或者访问将引发错误。 (2)绝大多数类支持无需显式初始化而定义对象,类提供了一个合适的默认值(eg:string默认值为空串“”)
值初始化:
列表初始化: 用花括号来初始化变量:(如下都是) 在使用内置类型变量时,初始化有一特点:如果使用列表初始化且初始值存在丢失信息的风险时,编译器将报错;
| |||
2.2.2 变量声明和定义的关系 | |||
分离式编译,将声明与定义分开 声明(declaration):使得名字为程序所知 定义(definition):负责创建与名字关联的实体; 相同点:都规定了变量的类型和名字(声明一个变量而非定义:关键字extern) 不同点:定义还申请了存储空间,还可能为变量赋一个初始值(任何包含了显式初始化的声明即成为定义)
在函数内部,如果试图初始化一个extern标记的变量,将引发错误(防止两次初始化)
变量能且仅能被定义一次,但是可以多次声明 | |||
2.2.3 标识符 | |||
2.2.4 名字的作用域 |
//声明与定义
extern int i; //声明而非定义
int j; //声明并定义
2.3 复合类型(参考指针与引用)
2.4 const限定符
const对象创建之后其值不能再改变,因此const对象必须初始化
| |
const和初始化 | |
在不改变const对象的操作中包括初始化,用一个对象初始化另外一个对象,是不是const无关紧要; 一旦初始化完成,两个对象就没什么关系 | |
2.4.1 const的引用 | |
不能一个非常量引用指向一个常量对象,如果该初始化合法,则可以通过非常量引用改变常量对象的值,不正确 | |
初始化和对const的引用 | |
1、初始化常量引用时允许任意表达式作为初始值(非常量对象、字面值),只要该表达式的结果能转换成引用的类型,如double转int即可。 程序中,ri绑定的是一个临时量对象(编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未知名对象)
当ri不是常量时,绑定的是一个临时量,还是不能通过ri改变dval的值,无意义,C++也把这种行为归为非法行为。
这就是为啥 可以用常量的引用绑定一个非常量(其实绑定的是一个临时量) 不能用非常量的引用绑定一个常量(无意义,定为非法) | |
指针和const(与引用类似) | |
1、指向常量的指针不能用于改变其所指的对象 2、存放常量对象的地址,只能使用指向常量的指针 3、允许另一个指向常量的指针指向一个非常量对象 (使用常量的指针仅仅要求不能通过该指针改变对象的值,并没有规定那个对象的值不能通过其他途径改变)
| |
2.4.4 constexpr和常量表达式 | |
定义:值不会改变并且在编译过程就能得到计算结果的表达式,包括字面值(看数据类型和初始值)
| |
constexpr变量 | |
C++新标准规定,允许将变量声明为constexpr类型以便由编译器验证变量的值是否是一个常量表达式 | |
字面值类型 | |
包括算术类型,引用,指针 一个constexpr指针的初始值必须是nullptr或者0,或者是存储于固定地址中的对象
函数体内定义的变量一般来说并非存放于固定地址中,因此constexpr指针不能指向这样的对象;相反,定义于函数体之外的对象其地址固定不变,能用来初始化constexpr指针;
| |
指针与constexpr | |
1、在constexpr中声明了一个指针,限定符只对指针有效,与指针所指对象无关
|
//常量创建之后必须初始化
const int i = get_size(); //不是常量表达式
const int j; //错误
//const与初始化
int i = 42;
const ci = i; //正确
int j = ci; //正确
顶层const:
- 顶层const表示指针本身是一个常量,底层const表示指针所指对象是一个常量,更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型的基本类型有关;指针类型即可以是顶层const,也可以是底层const
int i = 0;
int *const p1 = &i; //不能改变p1的值,顶层const
const int ci = 42; //不能改变ci的值,顶层const
const int *p2 = &ci;//允许改变p2的值,底层const
const int *const p3 = p2; //靠右的是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的都是底层const
- 拷贝时候的区别:顶层const不受影响,执行拷贝操作不会改变被拷贝对象的值;底层const受影响,拷入与拷出的对象必须具有相同的底层const,或者两个对象的数据类型可以转化(一般,非常量可以转化为常量,反之不行)
int *p = p3; //错误:p3包含底层const的定义,而p没有
p2 = p3; //正确:p2和p3都是底层const
p2 = &i; //正确:int* 可以转化为const int*
int &r = ci; //错误:普通的int&不能绑定到int常量上
const int &r2 = i; //正确:const int&可以绑定到普通int上
2.5 处理类型
2.5.1 类型别名 | |
1、关键字typedef 2、关键字using(C++11)
| |
2.5.2 auto类型说明符(C++11) | |
auto让编译器通过初始值推算变量的类型,auto定义的变量必须有初始值 | |
2.5.3 decltype类型指示符(C++11) | |
作用:选择并返回操作数的数据类型,编译器分析表达式并得到它的值,却不实际计算表达式的值
处理顶层const: 1、auto一般会忽略顶层const,但是底层const会被留下来 2、如果decltype使用的表达式是一个变量,则dectlype返回该变量的类型,包括顶层const和引用 |
第三章、字符串、向量和数组
string:支持可变长字符串
vector:支持可变长集合
iterator;配套类型
3.1 命名空间的using声明
3.2 标准库类型string
string和vector | |
string:可变长字符序列 vector:某种给定类型对象的可变长序列 | |
命名空间的using声明 | |
1、作用域操作符:: 意思是编译器从操作符左侧名字所示的作用域中寻找右侧那个名字 std::cin,使用命名空间中的cin
| |
初始化string对象的方式 | |
直接初始化与拷贝初始化 使用=号初始化变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去 不适用=号,使用的是直接初始化
| |
string对象上的操作 | |
读写string对象
| |
string的empty和size操作 | |
string::size_type:size()返回类型,无符号类型的值,可以存放下任何string对象的大小
!!!一个表达式中已经有了size()函数,就不要使用int,假设n是负值的int,s.size() < n 一般返回true,因为负数n自动转换为一个比较大的无符号数 | |
字面值和string对象相加 | |
1、C++中的字符串字面值并不是标准库类型string的对象,字符串字面值同string是不同类型 2、标准库类型允许把字符字面值和字符串字面值转换成string对象 3、当把string对象和字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string
| |
范围for语句 | |
C++11 for(auto c: str) for(auto &c : str) |
//读取未知数量的字符串
int main(){
string word;
while(cin >> word){
cout << word << endl;
}
return 0;
}
//使用getline读取整一行
int main(){
string line;
while(getline(cin, line)){ //保留输入的空白符,从给定的输入流中读入内容,直到遇到换行符
cout << line << endl;
}
return 0;
}
标准库类型vector
vector | |
1、类模板(而非类型,vector<int>是类型) 编译器根据模板创建类或函数的过程称为实例化
2、引用不是对象,所以不存在包含引用的vector | |
定义和初始化vector对象 | |
c++ primer p87 1、默认初始化 2、直接初始化 3、复制初始化 4、列表初始化(其实调用拷贝构造函数) 例外: 1、使用拷贝初始化时(使用=),只能提供一个初始值; 2、提供的是一个类内初始值,只能使用拷贝初始化或使用花括号形式的初始化 3、如果提供的是初始值列表,只能把初始值放在花括号里进行列表初始化,不能放在圆括号里
值初始化 | |
向vector中添加元素 | |
如果循环体内包含有向vector对象添加元素的语句,不能使用范围for 范围for语句等价的语句:
范围for语句中,预存了end()的值,一旦在序列中添加元素,end()的值可能变得无效 | |
vector操作 | |
vector.size():返回值是由vector定义的size_type类型 vector<int>::size_type //正确 vector::size_type //错误
下标的类型是相应的size_type类型,vector[0]
|
迭代器介绍
使用迭代器 |
begin():返回指向第一个元素的迭代器 end():返回尾后迭代器
若容器为空,则begin()和end()返回同一迭代器,都是尾后迭代器
前缀++改变了对象的状态并返回对象改变后的状态,不需要创建临时对象。 后缀++改变了对象的状态但是返回的是对象改变前的状态,并且需要创建一个临时对象。
迭代器尽量使用前缀++
迭代器类型 vector<int>::iterator it; //能读写 vector<int>::const_iterator it; //能读,不能写
begin和end的返回类型由对象是否为常量决定
c++11中,cbegin和cend,不管vector本身是否为常量,返回值都是const_iterator
!!!但凡使用了迭代器的循环,不要向迭代器所属的容器添加元素,会使迭代器失效 |
结合解引用和成员访问的操作 |
(*it).empty() //解引用,然后调用结果对象的empty成员 *it.empty //错误,it是迭代器,没有empty成员 it->empty //等价于(*it).empty(), 使用箭头运算符-> |
迭代器运算 |
iter1 - iter2 //得到两迭代器距离 //只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就可以相减,类型是difference_type的带符号整型数,可正可负 |
//依次处理字符直到遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it){
*it = toupper(*it);
}
3.5 数组
参考C和指针中数组部分https://blog.csdn.net/a1059682127/article/details/103692849
第4章 表达式
重载运算符 | |||
运算符作用于类类型的运算对象,可以通过运算符重载重新定义该运算符的含义。使用运算符重载可以令程序易于编写和阅读
| |||
左值和右值 | |||
C语言:左值可以位于赋值语句左侧,右值则不能 C++: 1、当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置) 2、在需要右值的地方可以用左值来代替(实际使用的是其值),但是不能把右值当成左值使用 | |||
4.1.1 优先级与结合律 | |||
4.1.3 求值顺序优先级:规定了运算对象的组成方式,eg:高优先级运算符的运算对象要比低优先级运算符的运算对象更为紧密结合在一起 4种运算符明确规定了运算对象的求值顺序: 1、&& 先求左侧运算对象的值,只有当左侧运算对象的值为真时才继续求右侧运算对象的值 2、|| 先求左侧运算对象的值,只有当左侧运算对象的值为假时才继续求右侧运算对象的值(短路求值) 3、条件运算符?:把简单的if-else运算逻辑嵌入到单个表达式中:cond ? expr1 : expr2; //只对表达式expr1, expr2中的1个求值 4、,逗号运算符 | |||
4.2 算术运算符 | |||
优先级:一元运算符>乘除>加法减法 一元运算符:&, *(解引用), +expr(一元正号), -expr(一元负号) 乘除:*,/, %
当一元正号作用于一个指针或算术值时,返回运算对象值的一个(提升后)的副本 | |||
4.3 逻辑和关系运算符 | |||
注意 if(i < j < k) //拿i < j 的布尔值结果跟K比较 正确方式:if(i < j && j< k) | |||
4.4 赋值运算符 | |||
1、赋值运算符的左侧运算对象必须是一个可修改的左值
赋值运算符优先级低于关系运算符的优先级 · | |||
4.5 递增和递减运算符 | |||
前置版本:将运算对象+1,然后将改变后的对象作为求值结果 后置版本:将运算对象+1,返回改变之前的对象值的副本 除非必须,否则不用递增递减运算符的后置版本 why?前置版本把值+1然后返回改变了的运算对象;后置版本需要将原始值存储下来以便返回未修改的内容
后置递增运算符的优先级要高于解引用运算符,*pbeg++等价于*(pbeg++) | |||
4.6 成员访问运算符 | |||
箭头运算符:ptr->mem等价于(*ptr).mem 点运算符: | |||
4.7 条件运算符 | |||
条件运算符?:把简单的if-else运算逻辑嵌入到单个表达式中:cond ? expr1 : expr2; //只对表达式expr1, expr2中的1个求值
条件运算符的优先级非常低,通常需要在两端加上括号
| |||
4.8 位运算符 | |||
4.9 sizeof 运算符 | |||
sizeof运算符返回的是一条表达式或者一个类型名字所占的字节数,所得值是一个size_t类型的常量表达式
| |||
4.11 类型转换 | |||
隐式类型转换 数组转换成指针:数组自动转换成指向数组首元素的指针 但是decltype关键字,取地址符&,sizeof,dypeid等时,不会发生数组转换成指针
指针转换: 常量整数值0、字面值nullptr可以转化为任意指针类型, 任意指针类型可以转为void*; 任意对象的指针能转化为const void*
常量转换: 指向非常量类型的指针转换成指向相应常量类型的指针; 引用。。。
| |||
4.11.3 显式转换 | |||
强制类型转换cast: cast_name<type>(expression)
4种
| |||
static_cast | |||
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
static_cast强制转换的结果将于原始的地址值相等,因此必须确保转换后所得的类型就是指针所指的类型 | |||
const_cast | |||
const_cast只能改变对象的底层const,不能改变表达式类型;=
| |||
reinterpret_cast | |||
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释
reinterpret_cast体现了 C++ 语言的设计思想:用户可以做任何操作,但要为自己的行为负责。 (比如上述程序可以通过,但是编译器不知道pc真实指向的是什么) | |||
dynamic_cast运算符 | |||
使用形式: dynamic_cast<type*>(e); //e必须是一个有效的指针 dynamic_cast<type&>(e); //e必须是一个左值 dynamic_cast<type&&>(e); //e不能是左值 其中,type必须是类类型,并且通常情况下该类应该含有虚函数
e必须满足下列条件之一: 1、e的类型是目标type的公有派生类 2、e的类型是目标type的公有基类 3、e的类型就是目标type的类型
dynamic_cast的目标类型是指针的话,失败了为0;目标类型是引用的话,失败了抛出一个bad_cast异常
用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象。
dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。
那该如何判断引用转换是否安全呢?不存在空引用,因此不能通过返回值来判断转换是否安全。C++ 的解决办法是:dynamic_cast 在进行引用的强制转换时,如果发现转换不安全,就会拋出一个异常,通过处理异常,就能发现不安全的转换。 | |||
旧的强制类型转换 | |||
将类型名作为强制类型转换运算符的做法是C语言的老式做法 type (expr); //函数形式的强制类型转换 (type) expr; //C语言风格的强制类型转换
C++ 引入新的强制类型转换机制,主要是为了克服C语言强制类型转换的以下三个缺点。 1) 没有从形式上体现转换功能和风险的不同。 从Int到double是没有风险的,但是从基类指针转换为派生类指针,或者常量指针转换为非常量指针,都是有风险的 2) 将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。 3) 难以在程序中寻找到底什么地方进行了强制类型转换。 | |||
4.12 运算符优先级表(p147) | |||
作用域>成员访问,下标>后置++>前置++,一元正负号,解引用,取地址,sizeof>乘除法>加减法>左右移位>关系运算符>位运算符>逻辑运算符>条件运算符>赋值运算符 |
//来自c.biancheng.net/view/410.html
#include <iostream>
using namespace std;
class A
{
public:
operator int() { return 1; } //函数操作符,可以实现对象当函数使用
operator char*() { return NULL; }
};
int main()
{
A a;
int n;
char* p = "New Dragon Inn";
n = static_cast <int> (3.14); // n 的值变为 3
n = static_cast <int> (a); //调用 a.operator int,n 的值变为 1
p = static_cast <char*> (a); //调用 a.operator char*,p 的值变为 NULL
n = static_cast <int> (p); //编译错误,static_cast不能将指针转换成整型
p = static_cast <char*> (n); //编译错误,static_cast 不能将整型转换成指针
return 0;
}
变量:
1、当i是一个整数的时候i++和++i那个更快?它们的区别是什么?(1)
前置版本:将运算对象+1,然后将改变后的对象作为求值结果 后置版本:将运算对象+1,返回改变之前的对象值的副本 |
2、32位,64位系统中,各种常用内置数据类型占用的字节数?(1)
转载:https://www.cnblogs.com/reality-soul/p/6141074.html 在32位、64位系统当中,唯一改变的是指针的长度;在32位系统当中是4个字节、64位则是8个字节。所谓的32位、64位,这个指的是寄存器的位宽。 32位编译器: char :1个字节 64位编译器: char :1个字节 |
3、结构体内存对齐问题?结构体/类大小的计算?(1)
4、结构体struct和共同体union(联合)的区别(2)
5、C++的四种强制转换(2)见前面笔记
参考:https://www.cnblogs.com/chio/archive/2007/07/18/822389.html
static_cast |
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
它主要有如下几种用法:
注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。 |
dynamic_cast |
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全(dynamic_cast有运行时检查)。
|
// Leetcode.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include<iostream>
using namespace std;
class CBasic {
public:
virtual int eat() { return 0; }
};
class CDerived : public CBasic {
public:
virtual int eat() { return 1; }
};
int main() {
CBasic basic;
CDerived derived;
CBasic *ptr1 = &basic;
CDerived *ptr2 = &derived;
CBasic *ptr3 = &derived; //ptr3实际指向的是派生类
//使用dynamic_cast上行转换,ptr2实际指向的是派生类,转换成功
CBasic *pd1 = dynamic_cast<CBasic *>(ptr2);
if (pd1) cout << "pd1返回 " << pd1->eat() << endl;
else {
cout << "pd1转换不成功" << endl;
}
CBasic *pd2 = static_cast<CBasic *>(ptr2);
cout << "pd2返回" << pd2->eat() << endl;
//使用dynamic_cast下行转换,ptr1实际指向基类,含有不安全操作,dynamic_cast发挥作用返回null
CDerived *pd3 = dynamic_cast<CDerived *>(ptr1);
CDerived *pd4 = static_cast<CDerived *>(ptr1);
if (pd3) cout << "pd3返回 " << pd3->eat();
else {
cout << "pd3转换不成功" << endl;
}
cout << "pd4返回" << pd4->eat() << endl;
//使用dynamic_cast下行转换,ptr3实际指向派生类,没有问题
CDerived *pd5 = dynamic_cast<CDerived *>(ptr3);
if (pd5) cout << "pd5返回 " << pd5->eat() << endl;
else {
cout << "pd5转换不成功" << endl;
}
return -1;
}
/*
pd1返回1
pd2返回1
pd3转换不成功
pd4返回0
pd5返回1
*/
6、C++中的基本数据类型及派生类型(2)
1)整型 int 2)浮点型 单精度float,双精度double 3)字符型 char 4)逻辑型 bool 5)控制型 void 基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。 类型修饰符包括: >short 短类型,缩短字长 >long 长类型,加长字长 >signed 有符号类型,取值范围包括正负值 >unsigned 无符号类型,取值范围只包括正值 |
7、什么是“引用”?申明和使用“引用”要注意哪些问题?(3)
1、"引用"其实是引用对象的别名,不是一个对象; 2、引用要初始化,引用不能修改绑定的对象 3、引用不是一种新的数据类型,因此它本身不占存储单元,系统也不会为它分配存储单元。 4、不能建立引用的数组,但是可以建立数组的引用
|
8、将“引用”作为函数参数有哪些特点?(3)
1、引用是别名,对引用形参的修改将作用在被引用的实参上; 2、使用一般变量(传值/传指针,传指针本质上是传值)给函数传递实参时,当函数发生调用时,需要给形参分配存储空间,形参是实参的副本 3、传递的是对象时,还需要调用对象的拷贝构造函数 因此,当传递的数据较大时,传引用比一般参数传递的效率要高 |
9、在什么时候需要使用“常引用”?(3)
1、提高函数参数传递效率,但是不希望被引用的数据在函数等调用中被改变 3、函数的参数设为const 类型,能正常的兼容常量作为参数传递的问题,反之则不行
例如: 因此,需要将其改为: 或者 |
10、将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?(3)
转载:https://blog.csdn.net/weibo1230123/article/details/82014538 好处:在内存中不产生被返回值的副本; 格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 } 规则:
|
11、分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。(3)
bool: if(a) or if(!a) //表明其为“逻辑”判断 int(short、int、long等): if(a == 0) //表明与0进行数值上比较 float: const EXPRESSION exp = 0.000001; if(a < EXP && a > -EXP) //浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较 point if(a == NULL)
| |
float与double的范围和精度以及大小非零比较 | |
1. 范围 2. 精度 |
//转载:https://www.cnblogs.com/marvin-notes/p/4369111.html
const float EPSINON = 0.000001;
// 非零判断
if ((f >= - EPSINON) && (f <= EPSINON))
{
// f == 0
}
// 大小比较
if ((abs(f1-f2) >= - EPSINON) && (abs(f1-f2) <= EPSINON))
{
// (f1-f2) == 0, 即 f1 == f2
}
12、int id[sizeof(unsigned long)];这个对吗?为什么?(3)
正确,正确 这个 sizeof是编译时运算符,编译时就确定了 ,且sizeof返回值是一个size_t的常量表达式,所以可以用来声明数组的维数 |
13、全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是
怎么知道的?(3)
全局变量:定义在函数体外的对象,存在于程序的整个执行过程,程序启动时创建,结束时销毁
局部变量:函数形参与函数体内定义的变量,其声明周期依赖于定义的方式
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。