封装:隐藏实现细节,使得代码模块化。
1、 封装是实现面向对象程序设计的第一步。
2、 封装就是将数据或函数等集合在类中。
3、 被封装的对象通常被称为抽象数据类型。
4、 封装又分为数据封装和方法封装。数据封装防止代码(数据)被无意中破坏,只能通过类提供的公共接口修改。方法封装对用户隐藏了方法的细节,只要接口不变,内容的修改不会影响到外部调用者的使用。
继承:扩展已存在的代码模块(类),进而实现代码重用,节省开发时间。
程序员创建新类,通过继承,从现有类中吸收其数据和行为,再根据新类的特征,赋予新的数据和行为,使得程序结构清晰,体现了良好的面向对象特性。
多态:实现一个接口,多种实现,即接口重用。
C++ 在程序执行时,将内存大致分为代码区,全局区,栈区和堆区四个区域。不同的区域存储不同的数据,赋予不同的生命周期,能够更灵活地进行编程。
代码区:存放函数体的二进制代码,由操作系统管理创建。代码区是共享的,对于频繁被执行的程序,只需要存有一份代码即可。
全局区:存放全局变量、静态变量、常量,在程序结束后由操作系统释放。
栈区:存放局部变量、函数的参数值,由编译器自动分配释放。
堆区:一般由程序员通过 new 开辟空间,进行分配和释放,若程序员不释放,则程序结束时由操作系统回收。
static:用于声明静态成员,只有一个变量拷贝,供类的所有对象共享。需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。
1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值
2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内
3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝
4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量
注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象
const:用于定义常量,可以保护被修饰的量,防止意外的修改,增强程序的健壮性。编译器通常不会为普通const常量分配存储空间,而是将它们保存在符号表中,没了存储与读内存的操作,效率较高,同时节省了空间,避免了不必要的内存分配。
const可以:
1.限定变量为不可修改。
2.限定成员函数不可以修改任何数据成员。
const修饰类的成员变量,表示常量不可能被修改
const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数
不可以同时修饰成员函数。
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
class D继承了class B,class A,class C,里面有classA的、classB的、classC的,那么顺序为继承的构造->成员的构造->自身的构造->逆序的析构
#include
using namespace std;
class A
{
public:
A()
{
cout<<"A的构造"<<endl;
}
~A()
{
cout<<"A的析构"<<endl;
}
};
class B
{
public:
B()
{
cout<<"B的构造"<<endl;
}
~B()
{
cout<<"B的析构"<<endl;
}
};
class C
{
public:
C()
{
cout<<"C的构造"<<endl;
}
~C()
{
cout<<"C的析构"<<endl;
}
};
class D:public B,public A,public C
{
public:
D()
{
cout<<"D的构造"<<endl;
}
~D()
{
cout<<"D的析构"<<endl;
}
A a;
B b;
C c;
};
int main()
{
D d;
return 0;
}
函数指针是指向函数的指针变量。
因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
函数指针的定义方式为:
返回类型 (*指针变量名)(参数列表);
含义:
代码示例:
int func(int x); /* 声明一个函数 */
int (*f) (int x); /* 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
函数指针有两个用途:调用函数和做函数的参数。
不同: 变量的静态初始化是预编译时对变量进行初始化,而动态初始化,是在程序运行时,根据需要,动态分配空间并赋值。
1、存储位置
动态变量:存储在内存出栈数据区
静态变量:存储在全局数据区(静态数据区)
2、生命期
动态变量:根据你定义的位置确定,比如你在一个函数中定义的,那么超出该函数范围变量将失效
静态变量:程序结束时才释放
3、作用域
动态变量:同样的要根据你定义的位置才能确定,和第二点的一样
静态变量:当前文件中有效
4、静态存储只会初始化一次
传值方式:将实参的值拷贝给函数或方法,在函数内对形参进行操作,操作的对象是实参的拷贝,对实参本身没有影响,在函数结束返回后,形参被丢弃释放,实参的内容不会被改变
void exchange1(int x,int y)
{
int temp;
temp=x;
x=y;
y=temp;
}
传址方式:将实参的地址传递给函数,在函数内对形参进行操作等同于对实参进行相同的操作,在函数调用结束返回后,形参被释放,实参的内容是对形参进行操作后的结果。
传址又分为:引用传递&指针传递
引用传递:
void exchange3(int &x,int &y)
{
int temp=x;
x=y;
y=temp;
}
int a=3,b=4;
exchange3(a,b);
引用即对象的别名,传对象的引用,用于把一个对象的地址作为参数传递过去,而不是对象本身。引用传递,避免了一次实参到形参的拷贝,提高了效率。
指针传递:
void exchange2(int *px,int *py)
{
int temp=*px;
*px=*py;
*py=temp;
}
int a=4,b=5;
exchange2(&a,&b);
变量的作用域是可以访问该变量的代码域。
有:语句块作用域、类作用域、函数原型作用域、函数作用域、命名空间及全局命名空间作用域。
不同点:
底层: 普通局部变量存放于栈区,超出作用域后,变量被撤销,其所占的内存也被回收;静态局部变量存放于静态数据存储区,全局可见,但是作用域是局部作用域,超出作用域后变量仍然存在。
组合和继承都是是面向对象中代码复用的方式。
组合是指在新类里面创建原有类的对象,重复利用已有类的功能。
继承是面向对象的主要特性之一,它允许设计人员根据其他类的实现来定义一个类的实现。
组合和继承都允许在新的类中设置子对象(subobject),只是组合是显式的,而继承是隐式的。组合和继承存在着对应关系:组合中的整体类和继承中的子类对应,组合中的局部类和继承中的父类对应。
既然继承和组合都可以实现代码的重用,那么在实际使用时又该如何选择呢?一般情况下,遵循以下原则:
除非两个类之间是“is - a”的关系,,否则不要轻易地使用继承,不要单纯地为了实现代码的重用而使用继承,因为过多地使用继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自它的子类,从而增加程序的维护难度和成本。
不要仅仅为了实现多态而使用继承,如果类之间没有“is - a”的关系,可以通过实现接口与组合的方式来达到相同的目的。设计模式中的策略模式可以很好地说明这一点,采用接口与组合的方式比采用继承的方式具有更好的可扩展性。
广义:容器、算法、迭代器
细分:容器、算法、迭代器、仿函数、适配器、空间配置器
STL库用过吗?常见的STL容器有哪些?算法用过几个?
STL包括两部分内容:容器和算法
容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器
序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap,
priority-queue, slist关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set
算法有排序,复制等,以及各个容器特定的算法
迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。
1
打开文件: 流对象.open(“文件路径”,打开方式);
判断是否打开成功: 流对象.is_open();
2
判断是否到结尾: 流对象.eof()
查看当前对象输入流的字符,但输入流对象的当前位置不变: 流对象.peek()
读入一行数据: 流对象.getline(数组名,sizeof(数组名))
读入单个字符: 流对象.get()
读入整个对象: 流对象.read((char*)&p, sizeof(Person));
3
写入单个字符: 流对象.put()
写入整个对象: 流对象.write((const char*)&p, sizeof(Person));
将一个字符插入到当前输入流对象位置: 流对象.putback()
4
关闭文件: 流对象.close();
https://www.icode9.com/content-4-1273393.html
友元函数的作用就是提供直接访问对象的私有成员的接口。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。
使用情况:
1)必须在类的说明中说明友元函数,说明时以关键字friend开头,后跟友元函数的函数原型,友元函数的说明可以出现在类的任何地方,包括在private和public部分;
2)注意友元函数不是类的成员函数,所以友元函数的实现和普通函数一样,在实现时不用"::“指示属于哪个类,只有成员函数才使用”::"作用域符号;
3)友元函数不能直接访问类的成员,只能访问对象成员;主要原因是友元函数没有传递隐藏的this指针,而类的成员函数传递了this指针,所以成员函数可以直接使用成员变量,所以友元函数不是成员函数。
4)友元函数可以访问对象的私有成员,但普通函数不行;
5)调用友元函数时,在实际参数中需要指出要访问的对象;
6)类与类之间的友元关系不能继承;
7)一个类的成员函数也可以作为另一个类的友元,但必须先定义这个类。
虚函数:virtual void func(){}
纯虚函数:virtual void func()=0;
虚函数用于基类和派生类的同名操作使其具有多态性;
纯虚函数是用来定义抽象类的。
纯虚函数:
纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承
包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象
普通函数是静态编译的,没有运行时多态
抽象类声明:
class Shape
{
public:
virtual double area() = 0;
}
接口声明:
class Channel
{
public:
virtual bool open() = 0;
virtual void close() = 0;
virtual bool send(char* buf, int len) = 0;
virtual int receive(char* buf, int len) = 0;
};
相同:C++ 接口是使用抽象类来实现的,所以本质上是一个特殊的抽象类。里面都需要有纯虚函数。
不同:接口不能有数据成员,以及成员函数必须全部public。
内联函数的作用:提高代码运行效率。
当遇到正常的函数调用指令时,程序会在函数调用语句之后立即存储指令的内存地址,将被调用的函数加载到内存中,复制参数值,跳转到被调用函数的内存位置,执行函数代码,存储函数的返回值,然后跳回到执行被调用函数之前保存的指令的地址。如果大量运行上述的小工作或功能时,会造成运行时间过多。
C++内联函数提供了另一种选择。使用inline关键字,编译器将函数调用语句替换为函数代码本身(称为扩展的过程),然后编译整个代码。因此,使用内联函数,编译器不必跳转到另一个位置来执行该函数,然后跳回。因为被调用函数的代码已经可用于调用程序。
适用于的情况:
this 指针是所有非静态成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
1.在重名时用this->成员变量名
2.成员函数需要返回对象的引用,用return *this;
生存周期: 对象或变量从诞生到消失的这段时间。
静态生存周期: 对象或变量从定义开始,到程序结束才销毁。如:全局变量、静态局部变量、静态成员变量
动态生存周期: 对象 或变量从定义开始,到语句块或函数结束时就被销毁。如:形参、普通局部变量。
c++中,我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
静态绑定和动态绑定是C++多态性的一种特性
1)对象的静态类型和动态类型
静态类型:对象在声明时采用的类型,在编译时确定
动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改
2)静态绑定和动态绑定
静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定
动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定
只有虚函数才使用的是动态绑定,其他的全部是静态绑定
数组指针(也称行指针),是个指针, 指向数组。
声明形式:int (*p)[n];
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
指针数组,是个数组,每个元素都是指针。
定义 int *p[n];
[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素。
class CyberSec{
double score;
char mark;
}
64位下double占8byte,char占1byte 对齐8byte 所以总共是16byte
参考链接:https://blog.csdn.net/WJUNSING/article/details/104610207/
class InfSec{
CyberSec cybersec;
int stuId;
}
24byte
重载是同名函数通过形参个数、类型进行区分的不同函数。
虚函数 是子类和父类同名、形参、类型也都相同的类内函数,父类中声明为virtual。
区别:
1.函数重载可以用于非成员函数和类的成员函数,而虚函数只能用于类的成员函数
2.函数重载可用于构造函数,而虚函数不能用于构造函数
3.如果对成员函数进行重载,重载的函数与被重载的函数应该是用一个类中的成员函数,不能分属于两个不同继承层次的类,函数重载处理的是横向的重载。虚函数是对同一类族中的基类和派生类的同名函数的处理,即允许在派生类中对基类的成员函数重新定义。虚函数处理的是纵向的同名函数。
4.重载的函数必须具有相同的函数名,函数类型可以相同也可以不同,但函数的参数个数和参数类型二者中至少有一个不同,否则在编译时无法区分。而虚函数则要求同一类族中的所有虚函数的函数名,函数类型,函数的参数个数和参数类型都全部相同,否则就不是重定义了,也就不是虚函数了
5.函数重载是在程序编译阶段确定操作的对象的,属于静态关联。虚函数是在程序运行阶段确定操作对象的,属于动态关联。
作为通常的原则,如果一个类定义了虚函数,那么它的析构函数就应当是virtual的。因为定义了虚函数则隐含着:这个类会被继承,并且会通过基类的指针指向子类对象,从而得到多态性。因此,基类的析构函数是否为虚将决定子类的对象是否被析构。
1)如果 A的析构函数不是virtual的,且使用父类指针或引用去调用子类对象,那么此时就不是先调用B的析构函数再调用A的析构函数,而是只有父类析构。
Output:
~A();
2)如果A 的析构函数为virtual,则先~B(),再~A()
Output:
~B();
~A();
不用virtual 的几种情况:
先父类后子类。
静态联编和动态联编的区别是main函数在调用某个函数的过程中,是否存在事先确定的哪个函数的差别。
(1)如果事先确定了,需要使用某函数,则为静态联编(编译的时候出现)。静态联编就是指被调用函数和执行调用函数之间的关系以及它们在内存中的地址在编译的时候已经确定,运行时不会发生变化。由于编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时才能确定将要调用的函数。要确切之道该调用的函数,就必须要求联编工作在程序运行时才能进行。
(2)运行到某函数时才确定,则为动态联编(运行的时候才出现)。
(1)使用一个创建完毕的对象来初始化一个新对象,如Point p2(p1);//此处是括号法
(2)值传递的方法给函数参数传值(传的是类对象)
int func(Point p)
{
}
int main()
{
Point p;
func(p);
}
(3)值方式返回局部对象(返回的是类对象)
值方式返回会先进行局部变量的复制再返回,如:
Point func()
{
Point p;
return p;
}
**什么情况下会调用拷贝构造函数(三种情况) **
系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)
生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数
调用拷贝构造函数的情形:
1)用类的一个对象去初始化另一个对象的时候
2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候
举例:
#include
#include
using namespace std;
class A{
private:
int data;
public:
A(int i){ data = i;} //自定义的构造函数
A(A && a); //拷贝构造函数
int getdata(){return data;}
};
//拷贝构造函数
A::A(A && a){
data = a.data;
cout <<“拷贝构造函数执行完毕”<
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
return a.getdata();
}
//参数是引用,引用传递,不调用拷贝构造函数
int getdata2(A &a){
return a.getdata();
}
//返回值是对象类型,会调用拷贝构造函数
A getA1(){
A a(0);
return a;
}
//返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
A& getA2(){
A a(0);
return a;
}
int main(){
A a1(1);
A b1(a1); //用a1初始化b1,调用拷贝构造函数
A c1=a1; //用a1初始化c1,调用拷贝构造函数
int i=getdata1(a1); //函数形参是类的对象,调用拷贝构造函数
int j=getdata2(a1); //函数形参类型是引用,不调用拷贝构造函数
A d1=getA1(); //调用拷贝构造函数
A e1=getA2(); //调用拷贝构造函数
return 0;
}
(1)“.”(类成员访问运算符)
(2)" .*“(类成员指针访问运算符)
(3) “::”(域运算符)
(4)“siezof”(长度运算符)
(5) " ?:”(条件运算符)
三种。
1、从静态存储区域分配。 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,静态变量,常量。
2、在栈上创建。 在执行函数时,函数内局部变量、函数的参数值的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、从堆上分配,亦称动态内存分配。 程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。若程序员不释放,则程序结束时由操作系统回收。
相同:都提供了数据的封装
区别:
构造时,先构造父类,再构造子类部分,因此父类和子类的构造函数都会被调用;
析构时,要分情况:
(1)(父类析构函数不是虚函数)使用父类指针指向子类对象,析构该子类对象时,只会调用父类析构函数,因为不具多态性;
(2)(父类析构函数不是虚函数)使用父类指针指向子类对象,析构该子类对象时,会先调用子类析构,再调用父类析构。
(3)(父类析构函数不是虚函数)使用子类指针指向子类对象,析构该子类对象时,会先调用子类析构,再调用父类析构。
表面:
delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数
用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
什么是“引用”: 引用就是对某个变量其别名。对引用的操作与对应变量的操作的效果完全一样。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。
注意: 声明一个引用的时候,切记要对其进行初始化。
注意: int (&arr)[10];是数组的引用,是可以这么写的;但是不能建立引用数组(数组里面全是引用元素),比如 int& arr[10];
左值和右值:
左值是内存中所占用的对象,右值是这个值代表的内容。
左值和右值的特性:
左值和右值的特性有两个不同:
1、左值长远,右值短暂
左值是我们一直要用到后面的东西,所以左值是临时变量,右值是很短暂的,就是用过就消失的,一般来说是表达式。
2、左值可写可读,右值可读
左值是可读可写的,就是我们既可以读出左值得表达结果,也可以对其进行赋值,但是右值只是可读的,比如,我们不能写3=a这种表达式。
什么情况是左值,什么情况是右值
(除了const的)变量都是左值;
常量,或者更广阔的,表达式是右值。
《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:
情况一、 需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
#include
using namespace std;
class Test
{
public:
Test (int, int, int)
{
cout <<"Test" <<endl;
}
private:
int x;
int y;
int z;
};
class Mytest
{
public:
Mytest():test(1,2,3)
{
cout << "Mytest" << endl;
}
private:
Test test;
};
int main()
{
Mytest test;
return 0;
}
因为Test有了显示的带参数的构造函数,那么他是无法依靠编译器生成无参构造函数的,所以没有三个int型数据,就无法创建Test的对象。Test类对象是MyTest的成员,想要初始化这个对象test,那就只能用成员初始化列表,没有其他办法将参数传递给Test类构造函数。
情况二、 需要初始化const修饰的类成员或初始化引用成员数据;
class Test
{
private:
const int a;
public:
Test():a(10){}
};
或
class Test
{
private:
int &a;
public:
Test(int a):a(a){}
};
当类成员中含有一个const对象或者是一个引用对象时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对它们的赋值,这样是不被允许的。
情况三、 子类初始化父类的私有成员;
#include
using namespace std;
class Test
{
public:
Test()
{
}
Test(int x)
{
int_x =x;
}
void show()
{
cout<<int_x<<endl;
}
private:
int int_x;
};
class Mytest:public Test
{
public:
Mytest():Test(10)
{
//Test(10);
}
};
int main()
{
Mytest mytest;
mytest.show();
return 0;
}
子类初始化父类的私有成员,需要在(并且也只能在)参数初始化列表中显示调用父类的构造函数。
define只是简单的字符串替换而typedef则是为一个类型起新名字,所以typedef更安全。
typdef和define区别
#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查
typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名
typedef (int*) pINT;
#define pINT2 int*
效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。
封装、继承、多态
为变量分配地址和存储空间的称为定义,不分配地址的成为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
变量的声明有两种情况:
定义性声明(defining declaration)是需要建立存储空间的。例如:int a在声明的时候就已经建立了存储空间。
引用性声明(referncing declaration)是不需要建立存储空间的。例如:extern int
a其中变量a是在别的文件中定义的。从广义的角度来讲声明中包含着定义,但是并非所有的声明都是定义。我们常常这么描述:把建立空间的声明称之为“定义”,而把不需要建立存储空间称之为“声明”。很明显我们在这里指的声明是范围比较窄的,也就是说非定义性质的声明。
面向过程是将解决问题的重点放在如何实现过程的细节方面,把数据和对数据进行操作的函数截然分开,以数据结构为核心,围绕着功能的实现或操作流程来设计程序,安全性较低、扩展升级麻烦,当问题的规模变大时,编程将很复杂;
面向对象将客观事物看做具有属性和行为的对象,通过抽象找出同一类对象的共同属性和行为,形成类。通过继承与多态可以方便地实现代码重用,大大缩短了软件开发周期,并使软件风格统一。
数据类型是一组性质相同的具有一定范围的值集以及定义于这个值集上的一组操作。数据类型有int,char,double,float,bool等内部数据类型,也有枚举、结构、联合、数组、类等自定义外部类型。
ADT是抽象数据类型,是基于已有类型而组合生成的复合数据类型,类正是抽象数据类型的描述形式。
数据抽象指以数据为中心, 把数据及在数据上的操作作为一个整体来进行描述;
信息隐藏指通过封装,把类对外部的接口放在类的公有部分,而数据放在类的私有部分中,防止用户在类外直接访问数据,从而屏蔽了类的实现细节,实现信息隐蔽。
友元可以是函数或类。友元提供了【一个类的成员函数】和【一般函数或另一个类的成员函数】之间进行数据共享的机制。
利:通过友元,一个一般函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。可以提高程序运行效率,实现信息共享。
弊:破坏了类的封装性和隐藏性,导致程序可维护性变差。
多态是指多个对象调用同一函数完成不同的行为。C++支持的多态有多种类型,重载(包括函数重载和运算符重载)和虚函数是其主要的方式。
STL是standard template library(标准模板库),由容器、迭代器、算法三部分构成。程序员可以重用STL容器、迭代器和算法实现通用的数据表示和操作,节约大量的开发时间和资源。
只有当二元运算符最左边的操作数是该类的一个对象,或当一元运算符的操作数是该类的一个对象时,才有必要进行运算符重载。(代码见专栏的其他文章)
声明虚函数:在基类的函数原型前加一个关键字virtual,在派生类中重写函数;
使用虚函数: 在main函数中用父类指针或引用调用子类对象,使用new来产生。
**虚函数在多态性中的作用和意义:**当一个类至少包含一个虚函数时,这个类便具有了多态性。获得多态性有两种方式:从基类继承虚函数和自定义一个虚函数。通过这两种方式,都可以获得多态性。多态性是指通过(基类)指针或(基类)引用调用函数,这种调用是动态解析的,即在编译期间确定调用哪个函数,所以灵活性更强。
虚函数是怎么实现的:
每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),
类的示例对象不包含虚函数表,只有虚指针;
派生类会生成一个兼容基类的虚函数表。
函数模板:数据类型参数化的函数定义,是一个函数族,代表一类函数。
模板函数:函数模板实例化后就创建了一个具体的模板函数。
定义语句:Enum Week{Mon,Tue,Wed,Thu,Fri,Sat,Sun};
也可以Enum Week{Mon=1,Tue,Wed,Thu,Fri,Sat,Sun};
,这样定义,就会对应整数1,2,3…
枚举类是对整数区间的自定义类型 ,一旦定义则不能被改变,常常代替整数常量使用,可以使程序更清晰持久。
使用:在使用时,也无需初始化,直接使用枚举符。
语法错误是对语言规则的违背。当编译器不能正确识别语句时,便会导致语法错误;由于语法错误都是在编译阶段出现的,所以又称编译错误。比如:语句末尾缺少分号等。
逻辑错误是算法上的错误,编译能够通过,程序可以运行,但往往产生不正确的结果,比如循环语句结束条件没写,造成死循环。
顺序、选择、循环。
运算符重载:对已有的运算符赋予了多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
C++扩展性:同一个运算符可以适应不同的数据类型,增强了C++的扩展性。
类作用域:类X的成员M在以下几种情况下具有类作用域:
(1) 在X的成员函数内出现标识符M,同时在该函数中也没有声明同名的局部作用域标识符。
(2) 在表达式x.M或者x::M中,类X的对象x通过作用符号“.”,”::”访问的M也具有类作用域,这正是在程序中访问对象的成员的最基本的方法。
(3) 在ptr->M这样的表达式中,其中ptr为指向类X的一个对象的指针。
文件作用域:在任何函数外部声明的标识符的作用范围为文件作用域。从声明标识符到文件末尾处的任何函数均可访问。
拓展:
函数原型作用域:在函数原型声明时形参的作用范围就是函数原型作用域。此作用域为c++程序中最小的作用域,生存周期最短。
块作用域:函数形参表中的形参的作用域:从形参表中的声明处开始,到整个函数体结束之处为止;函数体内声明的变量的作用域:从声明处开始,一直到块结束的大括号为止。
NULL(nullptr是C++11新引入的,也可以写),0,具体的地址
信息隐藏
对象
cin: istream的实例,连接到标准输入设备
cout:ostream实例,连接到标准输出设备,可重定向
cerr:ostream实例,连接到标准错误设备,不可重定向,输出是无缓冲的
clog:ostream实例,连接到标准错误设备,输出有缓冲
在理解C的存储类之前,首先要搞懂的概念有:作用域、生存周期、连接属性 C中的存储类说明符实际上是上述属性的不同组合。
auto、static、extern、register。
auto表明一个变量具有自动存储时期,该说明符只能用在具有代码块作用域的变量声明中,外部或内部链接属性。
register表明一个变量具有自动存储时期,只能用在具有代码块作用域的变量,请求一个变量存储在寄存器中快速使用,但是不能获得改变量的地址,外部或内部链接属性。
static表明一个变量具有静态存储时期,仅被其生命所在的函数所知,static变量在函数返回到它的调用者后仍然保留着变量的值,具有代码块作用域或文件作用域,内部链接属性。
extern表明一个变量具有静态存储时期,在生命一个在其他地方定义了的变量(该关键字用于全局变量),具有文件作用域,外部链接属性。
似乎还有(不确定)
Const 将数据定为不变的,在只能定义声明,以后不可改变其值。在指针中使用时,const的位置决定是指针本身不变还是指针指向的数据不变。
Volatile 说明数据除了可以被程序修改外还可以被其他代理修改,主要用于编译器优化。
Restrict 只能用于修饰指针,限定的指针被认为是提供了对其所在指向的数据块的唯一访问途径
乘法、指针、解引用。
1、要有继承2、要有重写3、父类引用或指针指向子类对象
了解其实现(模板类-计数-自动回收),4种指针(C++11废了auto还剩三种),以及循环引用问题
常用的实现方案有 4 种,这里分别将它们称为迭代反转法、递归反转法、就地逆置法和头插法。值得一提的是,递归反转法更适用于反转不带头节点的链表;其它 3 种方法既能反转不带头节点的链表,也能反转带头节点的链表。
1、迭代反转链表
该算法的实现思想非常直接,就是从当前链表的首元节点开始,一直遍历至链表的最后一个节点,这期间会逐个改变所遍历到的节点的指针域,另其指向前一个节点。
其余三个不再赘述。
关于sizeof(string),书上写着sizeof(string)=4;string的实现在各库中可能有所不同,但是在同一库中相同一点是,无论你的string里放多长的字符串,它的sizeof()都是固定的,字符串所占的空间是从堆中动态分配的,与sizeof()无关。
sizeof(string)=4可能是最典型的实现之一,不过也有sizeof()为12、32字节的库实现。 但是VC6.0测试后sizeof(string)=16.还是跟编译器有关。
sizeof是一个操作符,strlen是库函数
sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数
编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
首先,C++是C的超集,所以static在C中的用法 对于C++来说是全盘接受的,而两者的不同也就是C++中多出来的特性,而这些多出来的特性与C++面向对象的特性有关,或更具体的说,就是static在“类”中的意义和作用。
1:C语言中的Static使用:
1.1 C中static变量 的影响其实不在作用域而在于 其“存储方式”:
即在函数内定义的static变量,其作用域仍然在函数内;在函数外定义的static变量,其作用域也在函数外,属于变量所在的文件的作用域。
static 的静态存储方式使得:同一函数的所有static变量的“副本”都共用一个该变量。所以使用了static变量的函数一般是“不可再入”的,不是“线程安全”的。
1.2 C中static函数 的作用:
C中static函数还有另外一个名字,叫:内部函数。从名字也可看出,C中的static函数的定义将函数“局部化”,使其访问权限被限制在当前文件内部,这实际上说明的是一种“代码的隐藏机制”,通过和extern函数(外部函数)的联合使用,可以实现C语言中函数的模块化设计,这有点类似与C++ 中的Public和private关键字的味道。 当然,如果在C中,函数默认的访问权限是extern的,所以关键字extern往往省略不写。
2: C++类中定义的Static变量和函数
其实本质上与C中的情况一致,不在于作用域(还是类的作用域),而在于其“存储方式”决定了Static变量将与类存储在一起(因为类本身在内存中有其原型的内存空间),而类的实例将共享这一static变量,所以static变量也不是“线程安全”的。
而static成员函数的设置,主要是为了访问静态变量,或者完成某些独立于类实例的功能。
volatile告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取。指针和普通变量一样,有时也有变化程序的不可控性。
常见的例子:子中断服务子程序修改一个指向一个buffer的指针时,必须用volatile来修饰这个指针。(以下是摘抄的问题)
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2) 一个指针可以是volatile 吗?解释为什么。
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2)是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3)这段代码有点变态。这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是
你所期望的平方值!正确的代码如下:
a是一级指针,&a是二级指针
#incude
void main(void)
{
int a[5] = {1,2,3,4,5};
int ptr = (int)(&a+1);
printf(“%d,%d”,(a+1),(ptr-1));
return ;
}
输出结果:2,5
注意,只有当实际使用这些函数的时候,编译器才会去定义它们。
1.缺省构造函数
2.缺省拷贝构造函数
3.缺省析构函数
4.缺省赋值运算符
5.缺省取址运算符
6.缺省取址运算符const
(1)拷贝构造函数生成新的类对象,而赋值运算符不能
(2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算符中如果原来的对象中有内存分配,要先把内存释放掉。
(3)当类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不能使用默认的。
函数
除了void型,其他都函数占有的字节数等于函数的返回类型所占有的字节数,与函数体内部无关。
const char *array="schedule";
char array[]="schedule";
char array[]={"schedule"};
char array[]={'s','c','h','e','d','u','l','e'};
需要注意的是,第一种指针式字符数组前要加const;
第二、三种的判定方式是array[i]=='\0'而不是NULL
第四种不能用'\0'判断,因为越界了
(1) C++中有引用,C中不存在引用的概念。C中的&只作“”取址”用。
(2) C++支持函数重载,C不支持。这是因为C++在编译过程中对函数重命名的规则保证了重载函数在重命名后函数名的唯一性。
(3) C++在动态管理内存时有除malloc、free外还有new、delete关键字,C只有malloc、free。
(4) C++是面向对象的语言,主要特征有:“封装、继承、多态”。封装隐藏了实现细节,使得代码模块化;继承使得子类能够继承父类的数据和方法,实现代码重用;多态是“一个接口,多个实现”,通过用子类重写父类的虚函数实现接口的重用.C是面向过程的语言,考虑如何通过过程对输入进行处理得到输出。
1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;
引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的
2)引用只有一级,而指针可以有多级
3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作
引用传参的时候,传进来的就是变量本身,因此变量可以被修改
if(a>0)
if(b>0)
else
...
这里的else应该与第二个if匹配,而不是第一个
若一个函数的功能是对任意类型的数据做同样的处理,则将所处理的数据类型说明为参数,就可以把这个程序改写为函数模板。函数模板就是数据类型参数化的函数定义,代表的是一类函数。其参数都是抽象的。
用同一函数名定义多个函数,这些函数的参数个数、参数类型或参数顺序不同,这就是函数重载。每个重载函数的参数是具体的。但参数完全相同而返回值不同的函数不构成重载。
有一种特殊情况:重载模板函数。当编译器在处理重载模板函数的问题时,遵循的原则是:首选函数名、参数类型都匹配的具体函数,再找模板。
接口定义并标准化了人和系统等诸如此类事物彼此交互的方式。
成员函数的实现细节对客户代码是隐藏的,使得程序员不会写出依赖类的实现细节的客户代码。
程序更容易修改,只要类的接口保持不变,类的实现的改变不会影响客户。
加快编译速度,提高了维护性,使得代码变得清晰。
string.h 是C语言中字符串操作函数的头文件
cstring是C语言中strcpy之类的函数声明,包含此头文件就可以使用C语言风格的strcpy之类的函数。
string是C++中string类模板的声明
CString是MFC中定义的字符串类,MFC中很多类及函数都是以CString为参数的。
结构体:将不同类型的数据组合成一个整体,是自定义类型
共同体:不同类型的几个变量共同占用一段内存
1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;
共同体中的所有成员占用同一段内存,它们不能同时存在;
2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、
1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域
2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。
3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址
4)#define可以定义简单的函数,const不可以定义函数
1)overload,将语义相近的几个函数用同一个名字表示,但是参数和返回值不同,这就是函数重载
特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无
2)override,派生类覆盖基类的虚函数,实现接口的重用
特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)
3)overwrite,派生类屏蔽了其同名的基类函数
特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字
new/delete,malloc/free都是动态分配内存的方式
1)malloc对开辟的空间大小严格指定,而new只需要对象名
2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数
既然有了malloc/free,C++中为什么还需要new/delete呢?
因为malloc/free是库函数而不是运算符,不能把执行构造函数和析构函数的功能强加于malloc/free
野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存
成因:
1)指针变量没有被初始化
2)指针指向的内存被释放了,但是指针没有置NULL
3)指针超过了变量了的作用范围,比如b[10],指针b+11
内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
1)类的构造函数和析构函数中new和delete没有配套
2)在释放对象数组时没有使用delete[],使用了delete
3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露
4)没有正确的清楚嵌套的对象指针
什么是内存泄漏: 动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。
方法: malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界
volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。
map和set的底层实现主要通过红黑树来实现
红黑树是一种特殊的二叉查找树
1)每个节点或者是黑色,或者是红色
2)根节点是黑色
3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4)如果一个节点是红色的,则它的子节点必须是黑色的
5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
其中,特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。
-与
前者是从标准库路径寻找
后者是从当前工作路径
C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL),并入了大部分的C++ Technical Report 1(TR1)程序库。C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、decltype,和模板的大量改进。
auto
C++11中引入auto第一种作用是为了自动类型推导
auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作
decltype
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型,有实例如下:
nullptr
nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,
lambda表达式类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:
函数对象参数mutable或exception声明->返回值类型{函数体}
1、构造函数不能声明为虚函数
1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
2、析构函数最好声明为虚函数
首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
1)预处理:根据文件中的预处理指令来修改源文件的内容
2)编译:编译成汇编代码
3)汇编:把汇编代码翻译成目标机器指令
4)链接:链接目标代码生成可执行程序
map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。
unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。
类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)
(new-type) expression
new-type (expression)
隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:
static_cast、dynamic_cast、const_cast、reinterpret_cast
1)static_cast :编译时期的静态类型检查
static_cast < type-id > ( expression )
该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的
2)dynamic_cast:运行时的检查
用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用
dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。
dynamic_cast如果不能转换返回NULL
源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression)
3)const_cast
去除const常量属性,使其可以修改 ; volatile属性的转换
4)reinterpret_cast
通常为了将一种数据类型转换成另一种数据类型
深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。
vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。
vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。
在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。
1)栈 stack 存放函数的参数值、局部变量,由编译器自动分配释放
堆heap,是由new分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收
2)因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片
3)堆的生长空间向上,地址越大,栈的生长空间向下,地址越小
可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。
windows下直接使用vs的debug功能
linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
对比值传递,引用传参的好处:
1)在函数内部可以对此参数进行修改
2)提高函数调用和运行的效率(所以没有了传值和生成副本的时间和空间消耗)
如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。
用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。
但是有以下的限制:
1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁
2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak
3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。
通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
1)友元函数
有元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。
friend 类型 函数名(形式参数);
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
2)友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
friend class 类名;
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈
2)局部变量体积太大。
解决办法大致说来也有两种:
1> 增加栈内存的数目;增加栈内存方法如下,在vc6种依次选择Project->Setting->Link,在Category中选择output,在Reserve中输入16进制的栈内存大小如:0x10000000
2> 使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)
每种容器类型都定义了自己的迭代器类型,每种容器都定义了一队命名为begin和end的函数,用于返回迭代器。
迭代器是容器的精髓,它提供了一种方法使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。
vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。
vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。
list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等
vector::iterator和list::iterator都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。
函数的调用过程:
1)从栈空间分配存储空间
2)从实参的存储空间复制值到形参栈空间
3)进行运算
形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。
数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。
当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。
传值:传值,实际是把实参的值赋值给行参,相当于copy。那么对行参的修改,不会影响实参的值 。
传址: 实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。
1)整型 int
2)浮点型 单精度float,双精度double
3)字符型 char
4)逻辑型 bool
5)控制型 void
基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。
类型修饰符包括:
short 短类型,缩短字长
long 长类型,加长字长
signed 有符号类型,取值范围包括正负值
unsigned 无符号类型,取值范围只包括正值
后记:916复试没有出什么概念题,都是一些算法,比如排序算法啦,初始化列表啦,让你填空,改错,读程序。