C++复习
- C++数据类型:自定义类型 和 基本类型
- ISO C++标准并没有明确规定每种数据类型的字节数和取值范围,它只是规定它们之间的字节数大小顺序满足char<short<<int<<long
- 不同的编译器对基本类型有不用的实现方式,32位和64位的编译器字节数不同。
- char与int,short,long有所不同,ISO C++标准并没有规定它在默认(不加修饰)情况下是有符号的还是无符号的。它会因编译器的 不同而不同。因此,char,unsigned char和signed char是三种不同的数据类型。
- 两种浮点类型除了取值范围不同,精度也有所不同。float可以保存7位有效数字,double可以保留15位有效数字。
- 标识符
- 不能以数字开始,不能有特殊符号
- 以字母或者下划线开始
- 只能以字母、数字和下划线组成
- 不能是C++的关键字
- 不能以数字开始,不能有特殊符号
- 整形常量
- 十进制
- 八进制:以数字0开始 如028
- 十六进制:以0x开始 如0x32
- 实型常量:一般形式和指数形式
- 一般形式:12.3 -34.4
- 指数形式:0.345E+2表示0.345*102,-34.4E-3表示-34.4*10-3 其中E可以是大写或者小写
- 注意:指数表示一个实数的时候,整数和小数部分都可以省略,但不能同时都省略。如 .123E-1,12.E2都是正确的,但是 E-3 不正确
- 注意:实型常量默认是double型,如果后缀F(或f)可以使其成为float类型 如12.3f
- 字符常量:
- 转义字符
- \a 响铃
- \n 换行
- \t 水平制表符
- \v 垂直制表符
- \b 退格
- \r 回车
- \v 换页
- \\ 字符'\'
- \'' 双引号
- \' 单引号
- 八进制或者十六进制表示
- \nnn 八进制表示
- \xnnn 十六进制表示
- nnn表示3位八进制或者十六进制
- 变量:
- 声明变量只是让编译器认识这个变量,不分配内存。只有定义了以后才会分配内存。
- 全局变量默认值是0,局部变量的值默认是随机的,auto已经过时
- register 表示存放在通用寄存器中
- cin和cout格式控制
- 操纵符 添加iomanip头文件
- dec 十进制
- hex 十六进制
- oct 八进制
- ws 提取空白符
- endl 换行
- ends 插入空字符(相当于一个空格)
- setprecision(int) 设置浮点数的位数(包括)
- setw(int) 设置域宽
- cout << setw(5)<<setprecision(4) << 9.888383322 << endl;
- 操纵符 添加iomanip头文件
- typedef : typedef 已有的类型名 新类型名
- typedef int Year; Year y1; //将Year定义成int类型 其中Year y1;和int y1;相同.
- typedef double Area,Volune; //将Area和Volune定义成double类型
- enum :枚举类型 :enum 枚举类型名 {变量值列表}
- 如enum Weekday {SUN,MON,TUE,WED,THU,FRI,SAT};
- iostream.h和iostream区别:存不存在std名字空间
- iostream.h为非标准的输入输出流,这个.h的头文件是C语言格式的,由于当时还没有名字空间这个说法,所以也就不存在std这个名字空间标识符。所以用iostream.h 就用不着std或者using namespace std了。
- iostream为标准的输入输出流,它是C++规范的带有名称空间的头文件,它包含在std名字空间中。
- 名字空间 namespace 主要解决重命名问题
- 如
- 引用(别名)
- 声明引用时,必须同时对其进行初始化,使它指向一个已存在的对象
- 一旦一个引用被初始化后,就不能改为指向其他对象
- 内联函数 inline
- 内联函数不是在调用的时候发生控制转移,而是在编译时将函数体嵌入在每一个调用处。这样就节省了参数的传递、控制转移等开销。
- inline只是建议编译器将函数作为内敛函数处理,最终怎么处理由编译器自己决定。
- 带默认形参值的函数
- 有默认值的形参必须在形参列表的最后
- 注意:在相同的作用域里,不允许在同一个函数的多个声中对同一个参数的默认值重复定义。比如:默认参数值在原型声明中给出,定义中不能再给出默认形参值。(在此处的定义中,将默认形参值以注释的形式出现时好的习惯 如int Min(int a/*=5*/,int b/*=6*/){})
- 函数的重载
- 定义:两个或两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同。编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是函数的重载。
- 函数重载的依据是:形参类型和形参个数,返回值不能作为重载的依据。
- 调用C++系统函数
- 添加#include<cmath> math之前的c表示这个文件是一个继承自标准C的头文件
- C++也可以使用.h后缀的头文件,但是尽量不应该使用
- http://www.cppreference.com 可以查阅各种常用标准c++函数的原型、头文件和使用方法。
- wchar_t 宽字节
- setlocale(LC_ALL,"chs");//设置所有选项,chs中文简体
- wcout同cout 用来输出宽字符
- 块:以{开始,以}结束的,中间有几条语句的语句块,它是多条语句,但可以看做一条语句(一个功能)。
- 三目运算符
- 注意:三目运算符可以比较不同类型的数 如
int a = 1;float b = 2.1;cout << (a <b ? a : b); 输出值为1,cout << (a <b ? a : b); 输出值为2.1;
- 注意:三目运算符可以比较不同类型的数 如
- 成员变量和成员函数的属性:
- public:该类的对象可以直接访问这些成员
- private:该类的对象不能直接访问,必须通过公有的成员函数才能访问。
- protected:对于其他类是私有的不能访问,但是对于其子类是可以被访问的(与继承方式也有关系)。
- 将.h和.cpp分成两个文件的原因:
- 类的使用者可能不关心类在程序中的实现细节,他只阅读头文件就可以知道所有关于类的信息,可以直接使用。
- 一个.h文件可以同时被多个.cpp文件使用。
- const;对于不应当改变对象的成员函数都应该声明为const,这样当该成员函数试图去修改该对象的成员变量,编译器会提示错误。
-
class A{public:void fun(int x, int y)const{i = x;//提示错误:因为该函数被声明成const,不允许修改该类的成员变量i和j。j = y;cout << x << " " << y << endl;}private:int i;int j;};
-
- 构造函数
- 构造函数没有返回值
- 每个对象在创建的时候会自动调用该类的构造函数。
- 默认构造函数没有什么参数,不执行任何功能,它的作用只是构造一个对象。
- 一旦创建一个构造函数,默认构造函数就会自动被屏蔽。
- 析构函数
- 析构函数没有返回值
- 析构函数不能有参数
- 一个类只能有一个析构函数,无论你以什么形式来重载析构函数,都会导致出错。
- 对象的生命终止的时候,调用析构函数,用来销毁该对象。
- 对象数组:如A a[3];
- goto
-
int main(){int i = 1;number: //设置标号if (i > 10)return 0;i++;cout << i << " ";goto number; //跳转到number}
-
- while 条件满足就执行,一直到条件不满足
- while可能会不执行里面内容
- do..while 一直执行,直到条件不满足
- do..while 最少会执行一次
- 指针:存储地址的变量
- 因为指针是用来保存内存地址的变量,因此定义一个指针后一定要用它来保存一个内存。假如指针没有保存内存地址,该指针就是一个失控指针,它可以指向任何地址,并且对该地址的数值进行修改或者删除。(解决的方法就是将该指针的值赋为NULL)
- int *p=NULL后,p的值是00000000,(*p会运行出错) 该地址不存放任何数据,不会产生不好的后果。
- 指针注意:
- 指针地址:&p 即指针自身的地址
- 指针保存的地址‘:p 即指针保存的另一个变量的地址
- 指针保存的地址处的值:*p 即保存的另一个变量的值
- 为什么使用指针(指针的用途):操作大型数据和类时,由于指针可以直接通过内存地址直接访问数据,从而避免在程序中复制大量的代码,因此指针的效率最高。指针有三大用途:
- 处理堆中的大型数据
- 快速访问类的成员数据和函数
- 以别名的方式向函数传递参数
- 数据存储的方式:
- 栈区(stack):由编译器自动分配并且释放,该区域一般存储函数的参数值、局部变量的值等。当函数运行结束时,所有局部变量和参数都被系统清理掉了。
- 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。
- 寄存器区:用来保存栈顶指针和指令指针。
- 全局区(又叫静态区static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。全局区不能人为的释放,程序结束后由系统释放。(未初始化的全局变量和静态变量的值是0)
- 文字常量区:常量字符串就是放在这里,程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码。
- 注意:堆区是采用匿名的方式来保存数据的,只能通过指针才能访问到这些匿名的数据。所有堆区的安全性是最好的。
- 堆和栈的比较:
- 内存申请方式不同:
- 栈:由系统自动分配。
- 堆需要程序员自己申请,因此也需要指明变量的大小。
- 系统响应的不同:
- 栈:只有栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将提示 overflow,也就是栈溢出。
- 堆:系统收到程序申请空间的要求后,会遍历一个操作系统用于记录内存空闲地址的链表,当找到一个空间所申请空间的堆结点后,就会将该结点从记录内存空闲地址的链表中删除。并将该结点的内存分配给程序,然后在这块内存区域的首地址处记录分配的大小,这样我们在使用delete来释放内存的时候,delete才能正确地识别并删除该内存区域的所有变量。另外,我们申请的内存空间与堆结点上的内存空间不一定相等,这是系统就会自动将堆结点上多出来的那一部分内存空间回收到空闲链表中。
- 空间大小的不同:
- 栈:在WINDOWS下,栈是一块连续的内存的区域,它的大小是2M,也有说是1M,总之该数值是一个编译时就确定的常数。是由系统预先根据栈顶的地址和栈的最大容量定义为好。假如你的数据申请的内存空间超过栈的空间,那么就会提示overflow。因此,别指望栈能存储比较大的数据。
- 堆:不连续的内存区域。各块区域由链表将它们串联起来,这些串联的内存空间叫做堆,它的上限是由系统中有效的虚拟内存来定的。因此获得空间比较大,而且获得空间的方式比较灵活。
- 执行效率的不同:
- 栈:由系统自动分配,因此速度比较快。但是程序员不能对其进行操作
- 堆:由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来很方便。
- 执行函数时的不同:
- 栈:在函数调用时,第一个进栈的是被调用函数下一行的内存地址。其次是函数的参数,假如参数多于一个,那么次序是从右往左。最后才是函数的局部变量。 由于栈的先进后出原则,函数结束时正好与其相反,首先是局部变量先出栈,然后是参数,次序是从左到右,这时所有变量都已出栈,指针自然地指到第一个进栈的那行内存地址,也就是被调用函数的下一行内存地址。程序根据该地址跳转到被调用函数的下一行自动执行。 由于栈的先进后出原则,所以她永远不可能产生内存碎片,因为在上面的盘子没有拿完之前,下面的盘子是不可能抽出的。它们的排序是如此有序,弹出是也非常有序,碎片想要产生也是非常难的。
- 堆:堆是一大堆不连续的内存区域,在系统中由链表将它们串接起来,因此在使用的时候必须由程序员来安排。它的机制是很复杂的,有时候为了分配一块合适的内存,程序员需要按照一定的算法在堆内存中搜索可以的足够大小的空间,如果没有满足条件的空间,那么就要向系统发出申请增加一部分内存空间,这样就才有机会分到足够大小的内存,然后将计算后的数值返回。显然,堆的运行效率比栈要低得多,而且也容易产生碎片。但是好处是堆可以存储相当大的数据,并且一些细节可以有程序员来安排。
- 内存申请方式不同:
- new:数据类型 *p=new 数据类型;
- 由于计算机的内存是有限的,因此当没有足够的内存而无法访问new请求时,new会返回0,该返回值被赋给指针后,这个指针就是空指针,空指针不会指向有效数据。new除了返回空值之外,还会引发异常。
- int *p = new int(87);
- int *p = new int;
- delete:delete后的p还可以继续使用。
- 一块内存区域只能释放一次,第二次释放就会出错;但是第一次释放之后,将指针赋NULL,再次释放将不会报错。
- 在堆上new出来的对象,不会自动调用析构函数,需要显示的使用delete来调用析构函数。
- 内存泄露:没有删除一个指针就对其重新赋值,该指针指向新的内存空间,之前的将会被覆盖,此时之前的开辟的空间就无法访问,从而导致内存泄露。
- 析构函数调用注意:
- 栈:存储在栈中的对象,在出栈的时候自动调用析构函数。
- 堆:需要手动的调用delete来释放。
- 指针的加减运算:
- int *p=new int;
- p++;//p的地址加上sizeof(int)个字节
- p--;
- p=p-2;//p的地址加上2*sizeof(int)个字节
- 指针的赋值运算:
- int *p=new int;
- int *p1=new int;
- p=p1;
- 指针的相减:
- *p=p-p1;//p指向的内存空间保存的就是p和p1的地址差。
- 指针的比较运算:
- p>p1;//p的内存地址大于p1的内存地址。
- 常量指针:本身不可以修改,指向的目标可以被修改。
- int *const p=&a;//p只能保存a的地址,但是a的值可以改变。
- 如果定义指向对象的指针,不能改变使其指向其他的对象。
- 指向常量的指针:只是限制修改它指向的目标,它本身是可以修改的。
- int const *p=&a;//p可以指向其他的值,但是a的值不能通过*p改变,给a赋其他的值还是可以的。
- int const *p=&a;同const int *p=&a;
- 如果定义指向对象的指针,不能使用该指针改变类的成员变量,也不能调用该类的改变类的成员变量值的函数;可以调用不改变类的成员变量的函数。
- 指向常量的常指针:本身和指向的值都不能修改
- const int *const p=&a;
- 含有常量指针和指向常量的指针的所有特点。
- 引用:别名
- 因为是同一个地址,所以修改一个就会修改另一个。
- int &ra=a;//ra是a的别名,这个身份无法再改变,但是可以改变它引用的值。
- 注意:定义引用的时候,一定要对引用初始化。引用就像常量,只能对其初始化,不能对其赋值。
- 空引用:引用不会为空
- 栈:如果对象存放在栈中,那么对象超出作用域时,引用会和对象一起消失
- 堆:如果对象存放在堆中,因为堆中内存空间必需使用指针来访问,因此用不着引用。即使再定义一个该指针的引用,那么将指针删除并赋空后,该指针的引用也相应的赋空了。
- 注意:对于引用而言,如果引用的是一个临时变量,那么这个临时变量的生存期会不少于这个引用的生存期。
- 按值传递对象:在传递时候,会建立一个该对象的拷贝(调用复制构造函数);而从函数返回一个对象是,也要建立这个被返回的对象的一个拷贝。这样带来的内存开销非常大。
- 在传递的过程中会默认调用复制构造函数,创建一个临时某个对象的临时副本。
- 在函数返回时,传递该对象时创建的副本会被删除,这时候又会自动调用该对象的析构函数来释放内存。
- 将一个对象按值传递给一个函数,会调用两次复制构造函数和两次析构函数(一个给参数传值,一次返回值);
- 按址传递可以防止调用复制构造函数和析构函数,这样可以减小开销
- 按址传递对象:传递对象的实际地址,不用再创建对象,不会调用复制构造函数和析构函数,减小内存开销。
- 按址传递破坏了按值传递的保护机制,解决方式就是使用const指针
- 使用const指针可以防止任何试图对该对象所进行的操作行为,并且保证返回一个不被修改的对象。
- 引用传值:最简单方便
- 使用引用传值还是指针传值:
- 用指针:因为指可以为空,但是引用不能为空, 指针可以被赋值,但是引用只可以被初始化,不可以被赋为另一个对象的别名。想使一个变量记录不同对象的地址,就必须使用指针。
- 根据实际情况选择
- 指针和引用的区别:
- 指针可以为空,引用不能为空
- 指针可以被赋值,引用不能被赋值
- 指针可以指向堆中空间,引用不可以指向堆中空间
- 常量和引用用冒号语法进行初始化。
- 复制构造函数:
- A(A&a){n=a.n;m=a.m;}//默认复制构造函数
- 构造函数进行类型转换:
- A a;
- a=1000;(隐式转换)和a=A(1000);(显示转化)功能相同,都是调用A(int x)构造函数
- 可以在构造函数的声明之前加上explicit关闭这种功能(只有一个参数的时候才能这样默认转换)
- 深复制和浅复制:
- 浅复制:编译器提供的默认复制构造函数只是把传递进来的对象的每个成员变量复制到新的成员变量中去,这样两个对象中变量均指向传入的对象的那块内存区域。就会出现错误。
- 深复制:开辟新的内存,存放调用对象的成员变量
- 创建一个类,编译器会默认添加4个函数
- 默认构造函数
- 析构函数
- 默认复制构造函数
- 默认赋值运算符函数
- 运算符重载:operator(翻译:操作)
- 前置++
-
const Human& operator ++() { ++i; (*p)++; return *this; } //返回引用,防止值返回时的内存开销
- 注意:++a;相当于a.operator++(),相当于调用函数,有返回值,也可以赋值
-
- 后置++
- const Human operator ++(int) { Human old(*this); i++; (*p)++; return old;} //因为临时作用域结束的时候,就会释放,所以如果返回引用是空,所以返回值。
- 加减+ / -
- const Human operator +(const Human &r){return Human(i + r.geti(),*p+r.getp());} //用const修饰形参,则需要r对象调用的函数要被const修饰,如:int get () const;
- 赋值=
- const Human &operator =(const Human &r) { if (this==&r) { return *this;} i = r.geti(); *p = r.getp();return *this; }
- 赋值运算符重载不用开辟空间,而复制构造函数可以当做构造函数使用,要开辟空间。
- const Human &operator =(const Human &r) { if (this==&r) { return *this;} i = r.geti(); *p = r.getp();return *this; }
- 转换类型运算符
- operator int() { return i; } //调用时 Human man;int x=int(man);或者int x=(man);或者int x=man;
- operator int() { return i; } //调用时 Human man;int x=int(man);或者int x=(man);或者int x=man;
- 下标[]
-
char &operator [](int index){if (index >= 0 && index < length){return size[index];}else{cout << "超出范围" << endl;return size[length];}}
- 注意:
- 由于函数的参数即是数组的下标,因此该函数只能带一个参数,不可带多个参数。
- 由于下标运算符函数只限于本类的对象使用,因此不得将下标运算符函数重载为友元函数,且必须是非static类的成员函数。
-
- <<:编译器判断<<左边是ostream的对象,就认为是输出;左边不是ostream的对象,就认为是位移操作符
-
普通函数:ostream & operator <<( ostream &out, const Human &man){out << man.age << endl;out << man.heither << endl;return out;//由于cout是另一个类ostream的对象,ostream类没有公共的构造函数,//因此函数无法调用该类的复制构造函数,必须按引用的方法接受ostream的对象,并按引用的方式返回ostream对象}
-
友元函数:friend ostream& operator <<(ostream &out, const Human&man)//因为该函数含有其他类的对象,所以这个函数就不能成为类Human的成员函数。加friend可以解决这个问题{out << man.age << endl;out << man.heither << endl;return out;}
-
- >>
-
friend istream& operator >>(istream &in, Human&man)//因为该函数含有其他类的对象,所以这个函数就不能成为类Human的成员函数。加friend可以解决这个问题{in >> man.age;in >> man.heither;return in;}
-
- C++的运算符大部分可以被重载,但是有一些却不能重载。如“.”,"::","*","? :","#".
- “.”,"::","*"在c++中有特殊的意义,假如重载的话会引起一些麻烦。
- "#"是预处理标志,而不是运算符
- "? :"没有确定性,重载没有意义
- “.”,"::","*"在c++中有特殊的意义,假如重载的话会引起一些麻烦。
- 前置++
- 继承和派生:
- 单一继承和多重继承:趋避是基类的个数
- 单一继承:class Son :public Base{};
- 多重继承:class Son :public Base, public Base1{};
- 派生方式
- 公有派生:
- 公有->公有
- 保护->保护
- 私有->不可访问的
- 私有派生:
- 公有->私有
- 保护->私有
- 私有->不可访问的
- 公有派生:
- 单一继承和多重继承:趋避是基类的个数
- 派生类的对象可以赋值给基类对象
- 对象赋值
- Father fa; Son so; fa=so;//编译成功 so=fa;//失败
- 分析so=fa出错原因:赋值运算会调用operator =()函数,这个函数将运算符右边的对象成员赋值给运算符左边的对象,由于operator是左边的对象调用的,所以赋值操作以左边对象为准。这样就会出现问题,因为右边对象的成员比左边对象的成员少(派生类继承基类的信息,还有自己特有的成员),operator=()函数严格按照命令来进行赋值操作时,由于在右边对象中找不到指定的某个成员,所以会导致出错。
- Father fa; Son so; fa=so;//编译成功 so=fa;//失败
- 对象指针赋值
- Father fa; Son so; Father*p=&so; //编译成功 Son *p1=&fa;//失败
- 分析:因为派生类所占的存储空间通常比基类的对象大,原因是派生类除了继承基类的成员之外,还拥有自己的成员,所以在用基类的指针操作派生类的对象时,由于基类指针会像操作基类那样操作派生类对象,而基类对象所占用的内存空间通常会小于派生类对象,所以基类指针不会超出派生类对象去操作数据。 但是派生类指针指向基类对象,那么就会把一部分不属于基类对象的内存也包括进来操作,这样在使用该指针进行操作时,常常会删除或修改了基类对象之外的数据,产生一些不易察觉而且后果很严重的错误。所以派生类指针是不允许指向基类对象的。
- Father fa; Son so; Father*p=&so; //编译成功 Son *p1=&fa;//失败
- 基类的引用可以作为派生类对象的别名,但是反过来不行,派生类的引用不可以作为基类对象的别名。(Father &f=s;//编译成功 Son &s=f;//编译失败)
- 对象赋值
- 继承时构造函数和析构函数的执行顺序
- 单一继承:先调用父类构造函数,再调用子类的构造函数
- 多重继承:调用基类构造函数的顺序是继承时的顺序
- 多重继承容易造成二义性(比如同时从两个类继承了函数名一样的函数,到底调用哪一个),解决方式是在调用的函数之前加上想用的类名::(d.A::hello()//调用从A类继承来的那个hello函数)。
- 虚基类不会产生二义性(在继承的public之前加上virtual关键字)
- 当在子类中定义一个与基类同名的函数时,那么等于是告诉编译器,用子类的函数覆盖掉基类的全部同名函数,同时将它的重载函数隐藏起来。(子类重写基类的任何一个同名的函数,基类中重载的同名函数全部在子类中不能调用)
- 指向父类的指针可以指向子类的对象(把派生类对象赋给基类指针并且访问基类成员的方法)
-
class father{public:void jump()const { cout << "父亲可以跳十米" << endl; }void run()const { cout << "父亲可以跑万米" << endl; }};class son:public father{public:void jump()const { cout << "儿子可以跳十米" << endl; }void run()const { cout << "儿子可以跑万米" << endl; }void math()const{ cout << "儿子会数学" << endl; }};int main(){father *p = new son;//p调用的都是基类的函数p->jump();//调用基类函数p->run();//调用基类函数// p->math();//错误 不能调用子类特有的函数delete p;return 0;}
-
- 虚函数 virtual
-
using namespace std;class father{public:virtual void jump() const { cout << "父亲可以跳十米" << endl; }void run()const { cout << "父亲可以跑万米" << endl; }private:};class son:public father{public:void jump()const { cout << "儿子可以跳十米" << endl; }void run()const { cout << "儿子可以跑万米" << endl; }void math()const{ cout << "儿子会数学" << endl; }private:};int main(){father *p = new son;//p调用的都是基类的函数p->jump();//调用子类函数(因为调用的是虚函数)p->run();//调用基类函数// p->math();//错误 不能调用子类特有的函数delete p;return 0;}
- 分析p->jump();//调用子类函数(因为调用的是虚函数):jump()前面加上关键字virtual,表示该函数是有多种形态的,即该函数可能被多个对象所拥有,而且功能不一,换句话说多个对象在调用时产生的效果也不一样。那么 系统在执行到有关键字virtual的函数时就会自动判断是哪个对象调用了它,然后调用该对象的同名函数。
- 一个函数被说明成虚函数,在派生类中覆盖了该函数,那么该函数也是个虚函数(不管有没有加virtual),不过应该把它说明为虚函数,这样看起来更好懂些。
-
- 调用虚函数的方法:
- 只有使用指针和引用才能实现多态性(如果在虚函数没有采用指针或者引用,那么就无法实现动态联编)
class father{public:virtual void run()const { cout << "父亲可以跑万米" << endl; }};class son:public father{public:void run()const { cout << "儿子可以跑万米" << endl; }};void one(father fa){fa.run();//调用父类的函数 没有动态联编}void two(father *fa){fa->run();//调用子类的函数}void three(father &fa){fa.run();//调用子类的函数}int main(){father *p = new son;one(*p);two(p);three(*p);return 0;}
- 只有使用指针和引用才能实现多态性(如果在虚函数没有采用指针或者引用,那么就无法实现动态联编)
- 系统调用虚函数:
- 动态联编/运行时联编和静态联编
- 联编:将一个调用函数者连接上正确的被调用函数,这一过程叫做函数联编,一般简称为联编。
- 动态联编(加virtual):预先不知道调用那个对象的函数,运行时动态的选择调用那个对象的函数。father *p=new son;//如果father函数是虚函数,p调用实际的对象,而如果father函数不是虚函数,p调用的是father的函数和变量。(如果在虚函数没有采用指针或者引用,那么就无法实现动态联编)
- 静态联编(不加virtual):运行之前就确定好了那个指针指向那个对象,而且在运行时不能改变/编译时就解决了程序中的操作调用与执行该操作代码间的关系。(所以代码在编译和运行时是一样的)
- 区别:静态联编由于对象不用对自身进行跟踪,因此速度浪费比较小。 而动态联编虽然可以动态追踪对象,灵活性比较强。但是速度浪费也比较严重。
- 虚函数使用成员名限定可以强行解除动态联编
-
father *p = new son;p->run();//调用son的函数p->father::run();//调用father的函数(使用成员名限定,强制静态联编)
-
- 虚析构函数
- 一个派生类对象在创建时会首先调用基类的构造函数,然后调用该类的构造函数,一般情况下,在使用虚函数的时候,我们都会被派生类对象传递给指向基类的指针,那么假如指向派生类对象的指针删除时会发生什么情况?如果析构函数是虚函数,那么就会进行正确的操作,它会先调用派生类的析构函数,由于派生类的析构函数会自动调用基类的析构函数,因此构造的整个对象都会被销毁。(一般情况下,任何类的析构函数都可声明为析构函数,当指针删除时,系统会获得对象运行时的类型并调用正确的析构函数)
- 注意:
- 由于析构函数不允许有参数,因此它不可能实现重载,那么一个类就只能有一个虚析构函数。
- 只有基类的析构函数被说明为虚函数,那么派生类的析构函数无论说明与否,都自然成为虚函数
- 在c++中虚构造函数是不存在的,因此也无法声明。
- 如果基类中定义了虚函数,析构函数也应说明为虚函数。这样对内存的回收会更准确。
- 数组:是一组具有相同名称和类型的变量的集合
- int a[10];
- 给a[10]赋值;//a[10]实际上不存在,但是编译器却不管该位置放的什么数据它都要写入,这样就会导致不容易发现的错误。
- 给a[8]赋值;//编译器自动把偏移量(又叫下标或者编号)8与元素的大小(整型为4个字节)相乘,得出结果为32,那么它就是从数组开头移动32个字节,进行访问。
- 在C++中声明数组时,系统会自动生成一个指向该数组的指针,而该指针通常指向数组的第一个元素的内存 地址。因此数组虽然不能将数值传递给函数,但是却能够将内存地址传递给函数。
- void fun(int x[]);//x代表数组名,数组名看做是该数组的第一个元素的地址,[]说明接受的是数值,用于和其他类型区别。
- 一维数组函数传参
- 数组因为通常都比较大,所以为了节省内存,C++规定数组在程序中只能有一个原本,因此,数组在函数中不可能再创造一个副本的。
- void fun(int a[ ]);//简要数组声明
- void fun(int a[30]);//标准数组声明
- void fun(int *a);//指针声明
- void fun(int a[ ]);//简要数组声明
- 数组因为通常都比较大,所以为了节省内存,C++规定数组在程序中只能有一个原本,因此,数组在函数中不可能再创造一个副本的。
- int a[10];
- dynamic_cast<son*>(pf);//将父类指针pf装换成子类指针。
- 纯虚函数:虚函数被初始化为0。
- 纯虚函数无任何功能,不能调用它,因为它是抽象的。
- 纯虚函数只有被子类继承并赋予新功能后才能被使用。
- 抽象类:包含纯虚函数的类叫做抽象类。
- 从抽象类继承的类,必须为每一个纯虚函数赋予功能。
- 不能定义一个抽象类的对象。
- 但是可以定义一个指向抽象类的指针。(用来实现多态性)
- 抽象类可以派生出抽象类,因为如果子类没有将纯虚函数覆盖完,该子类还是抽象类。
- 假如确实某个基类的虚函数一定会被其他所有的派生类覆盖掉,那么不如将其设置为纯虚函数。
- 二分搜索:将一个排好序的数组,不断地分成两半,然后在可能包含我们所要查找的值的那一部分搜索。
- 要求数组必修是排序好的
- 假如数组有两个及两个以上的数字,那么二分算法将不能确定该返回那个值。
-
int find(int num,int a[],int len)//num将要搜索的值 a[]要查找的值 函数的返回的是找到的元素的下标{int min = 0, max = len-1;while (min<=max){int zhong = (min+max)/2;if (a[zhong]== num){return zhong;}else if (a[zhong]<num){min = zhong + 1;}else{max = zhong -1;}}return len;//没找到 返回数组的长度}
-
int find(int num, int a[], int min,int max)//num将要搜索的值 a[]要查找的值 函数的返回的是找到的元素的下标{if(min>max){return max;}int zhong = (min + max) / 2;if (a[zhong] == num){return zhong;}else if (a[zhong]<num){min = zhong + 1;find(num,a, min,max);}else{max = zhong - 1;find(num, a, min, max);}}
- 对象数组的初始化:
- Student students[3] = { Student("one",12), Student("two",33), Student("three",44)};
- 对象数组的构造函数的执行顺序是:从数组中第一个对象开始到最后一个数组。
- 对象数组初始化时要注意构造函数参数
- Student students[3] = { Student("one",12), Student("two",33), Student("three",44)};
- 一个对象的大小由它所包含的变量决定。
- 杨辉三角
-
int a[15][15];cin >> m;//输入行数for{for (int j = 0; j <= i; j++){if (j==0||j==i){a[i][j] = 1;}else{a[i][j] = a[i - 1][j] + a[i-1][j-1];}cout << a[i][j] << "\t";}cout << endl;}
-
(int i = 0; i < m; i++)
- 结构体与类区别:结构体成员默认为公有,类成员默认是私有。
- 链表:
- 静态链表:所有结点的数据在内存中的分布都是在编译时就确定好的,不会在运行时再进行动态地分配空间,只能在编译前修改指向结点的指针。
- 动态链表:运行起来后,才根据实际情况创建结点。
- 链表编写:
- 创建链表:
book *creat()//创建链表{book *head = NULL,*tail=NULL;int _num;float _price;while (1){cout << "输入书编号:(-1表示输入结束)" << endl;cin >> _num;if (_num >= 0){book *p = new book;p->num = _num;cout << "输入书价格:(-1表示输入结束)" << endl;cin >> _price;p->price = _price;//p->next = NULL;//如果在结构体里没有默认值,就有这句if (head==NULL){head = p;tail = p;}else{tail->next = p;tail = p;}}else if (_num == -1){return head;}else{cout << "输入有误,请重新输入!" << endl;//待写 清理缓存区}}return head;}
- 显示链表:
void Show(book *head)//链表显示{while (head){cout << "图书编号:"<<head->num <<" 图书价格:"<< head->price << endl;head = head->next;}}
- 删除结点
book* Delete(book *head,int num)//(每次删除一个结点)返回头结点的指针,因为如果删除了头结点,就得将删除后的头结点返回出来{book *cur = head;if (head->num==num){head = head->next;delete cur;cout << "操作成功!" << endl;return head;}while (cur->next){if (cur->next->num==num){book *del = cur->next;cur->next = del->next;delete del;cout << "操作成功!" << endl;return head;}else{cur = cur->next;}}cout << "没找到!" << endl;return head;}
- 插入结点
- 尾插法:
book*insert(book *head, int num)//返回头结点 因为有可能插在第一个{book *p = new book,*cur=head;cin >> p->num >> p->price;while (cur->next){cur = cur->next;}cur->next = p;return head;}
- 头插法:
book*insert(book *head, int num)//返回头结点 因为有可能插在第一个{book *p = new book,*cur=head;cin >> p->num >> p->price;head = p;head->next = cur;return head;}
- 中间插法:
book*insert(book *head, int num)//返回头结点 因为有可能插在第一个{book *p = new book,*cur=head;cin >> p->num >> p->price;if (p->num <=head->num)//头部插入{head = p;head->next = cur;return head;}while (1){if (cur->next == NULL)//尾部插入{cur->next = p;return head;}if (cur->next->num>p->num)//中间插入{p->next = cur->next;cur->next = p;return head;}cur = cur->next;}}
- 尾插法:
- 创建链表:
- 链表程序:
-
#include<iostream>#include<string>using namespace std;struct book{int num;float price;book *next = NULL;};book *creat()//创建链表{book *head = NULL, *tail = NULL;int _num;float _price;//while (1)for (int i = 0; i <= 20; i+=2){cout << "输入书编号:(-1表示输入结束)" << endl;//cin >> _num;_num = i;if (_num >= 0){book *p = new book;p->num = _num;cout << "输入书价格:(-1表示输入结束)" << endl;// cin >> _price;_price = i;p->price = _price;//p->next = NULL;//如果在结构体里没有默认值,就有这句if (head == NULL){head = p;tail = p;}else{tail->next = p;tail = p;}}else if (_num == -1){return head;}else{cout << "输入有误,请重新输入!" << endl;//待写 清理缓存区}}return head;}void show(book *head)//链表显示{while (head){cout << "图书编号:"<<head->num <<" 图书价格:"<< head->price << endl;head = head->next;}}book* Delete(book *head,int num)//(每次删除一个结点)返回头结点的指针,因为如果删除了头结点,就得将删除后的头结点返回出来{book *cur = head;if (head->num==num){head = head->next;delete cur;cout << "操作成功!" << endl;return head;}while (cur->next){if (cur->next->num==num){book *del = cur->next;cur->next = del->next;delete del;cout << "操作成功!" << endl;return head;}else{cur = cur->next;}}cout << "没找到!" << endl;return head;}book*insert(book *head, int num)//返回头结点 因为有可能插在第一个{book *p = new book,*cur=head;cin >> p->num >> p->price;if (p->num <=head->num)//头部插入{head = p;head->next = cur;return head;}while (1){if (cur->next == NULL)//尾部插入{cur->next = p;return head;}if (cur->next->num>p->num)//中间插入{p->next = cur->next;cur->next = p;return head;}cur = cur->next;}}int main(){book *head = creat();show(head);//head = Delete(head, 1);head = insert(head,21);show(head);return 0;}
-
- new/delete和malloc/free区别:
- malloc/free是标准库函数,new/delete是C++操作符
- new/delete会调用构造函数和析构函数,malloc/free则不会。所以在C++中用new/delete。
- 将C++字符串转换成C字符串:str.c_str()
- 将C字符串转换成整型数:atoi(str.c_str());
- 将C字符串转换成浮点型数:atof(str.c_str());
- 静态成员变量:
- 静态成员变量不属于某个对象,属于整个类所共有,所以在全局区开辟内存空间(在类外初始化,即在全局区开辟空间)。
- 静态成员在没有对象之前就存在。
- 静态成员函数:
- 静态成员函数由于属于整个类,所以它不能访问某个对象的成员变量,因为它没有指向该对象的this指针,但是它可以访问该类的静态成员变量。
- 要尽量类成员名限定来访问静态成员函数,尽量不要使用对象来访问。
- 静态成员函数可以被继承,基类和派生类都可以共享该静态成员函数。
- 类中的任何成员函数都可以访问静态成员函数,但是静态成员函数不能直接访问非静态成员函数,因为静态成员函数没有this指针。
- 静态成员函数不能是虚函数。
- 函数指针:类比数组名,函数名也是指向函数第一条指令的常量指针。
- 程序编译后,每个函数都有一个首地址,也就是函数第一条指令的地址。可以用一个指针保存这个地址,那么这个指针就是函数指针,该指针可以看做函数名。可以通过该指针调用函数。
- 一个指向函数的指针必须确保该函数被定义且分配了内存,否则它将指向一个空地址。
-
int min(int x, int y){return (x < y ? x : y);}int(*pmin)(int, int) = min;//int型函数指针pmin,指向函数min,这个函数带有两个int类型的参数,并返回一个int类型的值。
- 函数指针可以减少一些重复的代码,因为函数指针名可以看做函数名的代号,可以通过它来直接调用函数,所以函数指针经常会在条件或者判断语句中出现,以便于用户选择调用不同的名字但又类型和参数相同的函数。
- 函数指针可以指向某个函数,但是前提是被指向的函数的参数和返回值都与该函数指针被声明时的返回值和参数相吻合。
- 函数指针数组:
-
int(*p[5])(int, int);//声明了一个有5个元素的数组指针,该数组指针所指的函数必须有两个int参数,而且//要返回int值。它与函数指针的区别只是函数指针只可以存储任意一个函数的地址,而函数指针数组可以存储多个
-
- 函数指针作为函数的参数:
-
int min(int x, int y){return (x < y ? x : y);}int(*pmin)(int, int)=min;int fun(int(*p)(int x, int y), int xx, int yy)//返回一个int类型值,有三个函数,第一个是有两个参数的函数指针{return p(xx,yy);}int main(){cout<<fun(pmin,3,2);return 0;}
-
- 成员函数指针:(类内)
-
class Father{public:int set(int x, int y){a = x;b = y;return x;}void Show(){cout << "a:" << a << " " << "b:" << b << endl;}private:int a;int b;};int(Father::*pset)(int, int);int main(){Father fa;pset = &Father::set;cout << (fa.*pset)(3,4) << endl;return 0;}
-
- 成员函数指针数组:
- 要在类的特定的对象中调用成员函数指针
- 能不用成员函数指针就不用。
- '\0':'\0'是一个空字符标志,它的ASCII码为0;
- cin和cout都是以空字符为结束符。他们遇见空字符就会停止输入或者输出。
- 而cin.get的结束符是'\n',因此遇到空格不会结束,而是把空格也看做一个字符串。在遇到'\n'后,停止读入,并在数组末尾添加'\0'。(a[12]按这样方式,最多存储11个字符)
- 用双引号包括起来的字符串隐含了字符串结束标志'\0',因此不用手动去添加它。
- 空格的ASCII码为32。
- cin遇见不可见字符和空字符就停止输入,(不可见字符指的是制表符和空格)。
- 而cout则只是遇到空字符才停止输出。这就是为什么将空字符作为字符串结束标志的原因。
-
char c1[20] = {'A',32,'B'};char c2[20] = { 'A',0,'B' };cout << c1 << endl;//输出A Bcout << c2 << endl;//输出A(cout遇见空字符就会停止输出,所以B没有输出)
-
- char字符串:
-
char c[4] = { 'h','e','l','l' };//不是字符串,因为没有字符串结束标志'\0'char c[5] = { 'h','e','l','l','\0' };//是字符串
- C++不限制字符串长度,因此可以将字符串数组的长度定义为无限大。
-
- 字符串长度包括'\0':
- strlen:求可见字符串长度
- sizeof:求字符串共有多少个元素
-
char a[] = "hello world";cout << strlen(a) << endl;//输出11cout << sizeof(a) << endl;//输出12
- string型字符串(添加头文件<string>或者使用std::string)
- C++风格的字符串就是用对象来保存字符串的。
- string型字符串和char型字符串的区别:
- char型字符串比较使用strcmp函数。string类的成员函数中重载了比较运算符(==)之类的关系运算符。
- char型赋值用strcpy,string类中使用operator函数重载了运算符
- strcpy会将第二个参数中所有的字符,包括结束标志'\0'一块复制到第一个参数中区。
- 不可以直接将char型字符数组进行赋值操作。
- assign():选择string字符串的部分赋值给另一个字符串
-
string str1;string str2 = "abcdefg";str1.assign(str2,3,2);//将str2中第3个元素开始2个元素赋给str1.
-
- strcpy会将第二个参数中所有的字符,包括结束标志'\0'一块复制到第一个参数中区。
- char型合并用strcat,string用+=
- 部分合并:char用strncat,string用append方法。
- 部分替换:char用strncpy,string用replace
- char型字符串也就是C风格的字符串,它是由一串字符组成,结尾为字符串结束标志‘\0’。字符串名是第一个字符的地址,因此我们将字符串名作为参数传递到函数中时,其实就是将地址传递到函数中去。由于字符串的最后一个字符是\0’,因此我们不必传递字符长度,只要在函数中设置一个循环体,把结束字符作为循环结束的条件即可。
- 结构体的对象可以直接赋值。
- C++清空缓存区:
-
cin.clear();//清除cin流的错误状态cin.ignore(numeric_limits<streamsize>::max(),'\n');//清空输入缓存区,numeric_limits<streamsize>::max()表示缓存区的大小,遇见换行‘\n’停止清除
-
- 私有继承和包含:
- 私有继承可以使派生类可以重新定义基类的虚函数
- 如果想访问保护成员用公有继承,不想访问保护成员有包含
- 公有继承可以访问基类的保护成员
- 包含不能访问类的保护成员
- 公有、私有和保护继承的不同:
- 公有继承:
- 公有->公有
- 保护->保护
- 私有->不可直接访问(派生类的成员函数无法直接访问它们,只能通过从基类继承来的公有或保护成员函数来间接访问)
- 保护继承:
- 公有->保护
- 保护->保护
- 私有->不可直接访问
- 私有继承:
- 公有->私有
- 保护->私有
- 私有->不可直接访问
- 私有继承与保护继承的区别:保护继承利于继续派生,而私有继承则不利于继续派生。
- 公有继承与保护继承和保护继承都有所不同,公有继承后访问权限维持不变,基类的公有成员被派生类继承后还是公有的,基类的保护成员被派生类继承以后还是保护的。
- 所以无论以公有、私有还是保护的方式进行继承,在子类中都是不可访问的,不过子类可以用父类的函数来操作他们,假如父类没有提供这样的函数,那么就不能访问了。
- 公有继承:
- 私有成员:只允许来自类内部的访问,不允许任何来自该类外部的访问,派生类也是不允许访问的。
- 保护成员:私有成员比较保护成员则放宽可一条,可以允许在派生类进行访问。
- 私有成员与保护成员的区别就在派生类中,也就是说假如不往下派生,保护成员和私有成员没有区别。
- 数据成员必须在构造函数初始化列表中初始化:
- 没有默认构造函数的内嵌对象
- 引用类型的数据成员
- 常数据成员
- 派生类构造函数执行次序:
- 调用基类构造函数,调用顺序为被继承时的顺序(左->右)
- 对派生类的新增成员对象初始化,调用顺序是声明时的顺序。
- 调用顺序都和初始化列表中的顺序无关
- 如果自类中定义的函数与父类的函数同名,但具有不同的参数数量或者参数类型,不属于函数重载。这时子类中的函数将使父类中的函数隐藏,调用父类中的函数必须使用父类名称来限定。
- 只有在相同的作用域中定义的函数才可以重载。
- 友元类:
-
class A{public:friend class B;//B是A的成员函数 所以B得成员函数可以通过A的对象访问A的保护和私有成员int aa;private:int aaa;};class B{public:int dd;void prinf(){a.aaa = 3;//可以访问A的私有和保护成员a.aa = 9;}private:A a;//定义A的对象};
- 友元关系不可传递,不能继承,是单向的。
- 友元类是为了访问类的私有和保护成员
-
- 嵌套类:在一个类中嵌套另一个类,类中的这个类,只能在本类中使用(可以做成员变量或者函数的参数返回值),不影响类外的同名类
- 关键字volatile有什么含意 并给出三个不同的例子
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
- 多线程应用中被几个任务共享的变量
- 回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
- 假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
- 1)一个参数既可以是const还可以是volatile吗?解释为什么。
- 2)一个指针可以是volatile 吗?解释为什么。
- 3)下面的函数有什么错误:
- int square(volatile int *ptr)
- {
- return *ptr * *ptr;
- }
- 下面是答案:
-
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。 它是const因为程序不应该试图去修改它。
2)是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3)这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
- 当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。
-
int fun(){unsigned int a = 6;int b = -20;int c = a + b;//c=14unsigned int d = a + b;//d= 4294967282(a + b > 6) ? puts("> 6") : puts("<= 6");//">6"}
-
- #define dPS struct s *和typedef struct s * tPS; 那个比较好
- 答案是typedef更好,
思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
- 答案是typedef更好,
- c++多态:
- 多态有动态多态,静态多态,函数多态和宏动态
- 宏多态:#define ADD(A,B) (A)+(B)//可以操作多种数据类型
- 函数多态==重载
- 静态多态:为C++引入了泛型的概念,泛型编程被认为是“组件功能基于框架整体而设计”的模板编程。
- 动态多态:实现基础是继承机制和虚函数,通过抽象类的派生来实现多态。
- 多态有动态多态,静态多态,函数多态和宏动态
- 使用复制构造函数的情况
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化
- 引用做参数
-
void incr(int& var){cout << var << endl;}int main(){short v1 = 1;const int v2 = 2;int v3 = 20;incr(v1);//错误,v1不是intincr(v2);//错误,v2时const 当void incr(const int& var)时可以运行incr(v3);//正确incr(10);//错误 当void incr(const int& var)时可以运行return 0;}
- 调用非const类型的引用形参,实参必须不是const类型的,而且实参的类型和形参的类型应当一致。调用一个有const引用的参数的函数时,如果实参不是一个变量或者类型不匹配时,函数会创建一个无名的临时变量用来存储实参的值,并把这个形参作为该临时变量的引用。
-
- 类访问另一个类的私有成员和保护成员
- (非正规的方法)//#define private public或者#define private protected
class A{private:int num;};class B{public:void prin(){cout << a.num << endl;//#define private public或者#define private protected}private:A a;};
- 友元类
class A{friend class B;private:int num;};class B{public:void prin(){cout << a.num << endl;//#define private public或者#define private protected}private:A a;}
- (非正规的方法)//#define private public或者#define private protected
- STL中的容器:容器一般用模板类来实现。不过STL并没有采用面向对象的技术,所以在STL中并没有一个通用的容器类,各种具体的容器也没有统一的基类。
- STL中的容器分为两种:顺序容器 和 关联容器
- 顺序容器:将一组具有相同类型T的对象,以严格的线性组织在一起。顺序容器可以视为数组和链表的推广。
- 例如:vector<T>、 deque<T>、 list <T>
- 关联容器:提供一个key(键)实现对元素的随机访问,其特点是key是有序的,即元素是按预定义的键顺序(例如升序)插入的。关联容器具有从基于键的集合中快速提取对象的能力,其中集合的大小在运行时是可变的。关联容器可以视为关联数组、映射或者字典的推广,他们保存的都是键值对,给定了其中的一个称为键的值,就可以快速访问与其对应的另一个值的值。主要有以下4种:
- set<Key> (集合):支持唯一键值,并提供对键本身的快速检索;例如 set<long>:{学号}(set类的头文件<set>)
- multiset<Key> (多重集合):支持可重复键值,并提供对键本身的快速检索;例如 ste<string>:{姓名}(可能有同名的)(multiset类的头文件<set>)
- map<Key,T>:支持唯一Key类型的键值,并提供对另一个基于键的类型T的快速检索;例如map<long,string>:{(学号,姓名)}、{(电话号码,姓名)}等。(map类的头文件<map>)
- multimap<Key,T> (多重映射):支持可重复Key类型的键值,并提供对另一个基于键的类型T的快速检索;例如map<string,string>:{(姓名,地址)}、{(姓名,年龄)}等。(multimap类的头文件<map>)
- 容器适配器:(容器适配器不是独立的容器)只能是某种容器的变种,它提供原容器的一个专用的受限接口。特别是,容器适配器和普通容器的不一样是在于不提供迭代器。(再STL中有3种容器适配器):
- stack<T> (栈):只支持top()、push()、pop()。
- queue<T> (队列)
- priority_queue<T> (优先队列):也是一种队列queue,不过其中的每个元素都被给定了一个优先级,用来控制元素到达队首top()的顺序。默认情况下,优先队列简单地使用运算符<进行元素比较,top()返回最大的元素。
- 优先队列,并不要求其全部元素都是有序的,而只要求其第一个元素是最大的。
- 顺序容器:将一组具有相同类型T的对象,以严格的线性组织在一起。顺序容器可以视为数组和链表的推广。
- STL中的容器分为两种:顺序容器 和 关联容器
- 迭代器:迭代器是STL提供的对一个容器中的对象的访问方法,并且定义了容器中对象的范围。迭代器就如同一个指针。事实上,c++的指针也是一种迭代器。但是迭代器不仅仅是指针,因此不能认为迭代器一定就具有地址值。
- 迭代器和指针类似,可以用使用*操作符获取数据,还可以使用自增操作符等
- 迭代器有两个已经定义好的,一个是begin,另一个是end。可以通过容器的begin()操作和end()操作获取这两个位置。(begin()指向容器中的第一个元素,end()指向的是容器中的最后一个元素的下一个位置,也就是end所指向的并不是容器的元素。通常begin和end之间的范围就是迭代器的范围)
- 二叉树不是树
- 二叉树和树的区别:
- 二叉树可以为空,但树不能为空
- 二叉树中每个元素都恰好有两棵子树(其中一个或者两种可能为空)。而树中每个元素可有若干子树
- 二叉树中每个元素的子树都是有序的,也就是说,可以用左、右子数来区别。(不是树的根本原因)
- 二叉树和树的区别:
- 内存分区:
- 堆:由程序员手动分配和释放,完全不同于数据结构中的栈,分配方式类似于链表。由malloc(c语言)或new(C++)释放。若程序员不释放,程序结束时由系统释放。
- 栈:由编译器自动分配和释放的,存放函数的参数值、局部变量的值等。操作方式类似数据结构中的栈。
- 全局(静态存储器):存放全局变量和静态变量,包括DATA段(全局初始化区)与BSS段(全局未初始化区)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和未初始化的静态变量存放在BSS段。程序结束后有系统释放。
- char *n="hello"和char a[]="hello"的区别:
- 变量n位于栈上,其内容是一个地址,指向位于文字常量去的“hello”,此时“hello”在内存中只有一份拷贝;
- a是一个位于栈上的有6个元素(含字符串末尾的空字符)的数组,并将“hello”拷贝到它所占的内存中,此时“hello”有两份拷贝。
- 数组元素初始化时,若没有显式提供元素初值,则元素会被像普通变量一样初始化;
- 函数体外定义的内置类型数组(即内置类型的全局数组),元素初始化为0;
- 函数体内定义的内置类型数组,元素无初始化(注意,若只初始化部分元素,其后的元素此时也会被初始化为0)
- 如果不是内置类型,则不管其在哪里定义,自动调用默认的构造函数为其初始化,若该类型无默认构造函数则会报错。