默认构造函数,拷贝构造函数,析构函数,赋值运算符(operator =),取址运算符(operator &)(一对,一个非const,一个const)。
class Empty{ public: Empty(); //缺省构造函数 Empty(const Empty&); //拷贝构造函数 ~Empty(); //析构函数 Empty &operator = (const Empty &); //赋值运算符 Empty *operator &(); //取址运算符 const Empty* operator & () const; //取址运算符const }问题:哪些情况下会调用拷贝构造函数? (1)一个对象以值传递的方式传入函数体 (2)一个对象以值传递的方式从函数返回 (3)一个对象需要通过另外一个对象进行初始化 其实拷贝构造函数是由普通构造函数和赋值操作符共同实现的。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问。 注意: (1)单例类只能有一个实例 (2)单例类必须自己创建自己的唯一实例 (3)单例类必须给所有其他对象提供这一实例
单例模式需满足两个条件: (1)保证一个类只创建一个实例 (2)提供一个对该实例的全局访问点
如果系统有类似的实体(有且只有一个,且需要全局访问),那么就可以将其实现为一个单例。实际工作中常见的应用举例: (1)日志类:一个应用往往只对应一个日志实例 (2)配置类:应用的配置集中管理,比提供全局访问 (3)管理器:比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例 (4)共享资源类:加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。
c++实现单例模式 (1)懒汉模式 singleton在程序第一次调用的时候才会初始化
class Singleton{ private: static Singleton* instance; Singleton(){}; public: static Singleton* GetInstance(){ if(NULL == instance){ instance = new Singleton(); } return instance; } }; Singleton* Singleton::instance = NULL; //静态数据成员是静态存储的,必须对它进行初始化使用该模式时,由于if语句的存在,会影响调用的效率。而且,在多线程环境下使用时,为了保证只能初始化一个实例,需要用锁来保证线程安全性,防止同时多个线程进入if语句中。
加入double-check,代码变为:
class Singleton{ private: static Singleton* instance; Singleton(){}; public: static Singleton* GetInstance(){ if(NULL == instance){ Lock(); //借用其他类来实现,如boost if(NULL == instance){ instance = new Singleton(); } Unlock(); } return instance; } }; Singleton* Singleton::instance = NULL;Lock是确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待。(即被阻止),直到该对象被释放。
如果处理大量数据时,锁会成为整个性能的瓶颈。 一般懒汉模式适用于程序一部分中需要使用Singleton,且在实例化后没有大量频繁访问或线程访问的情况。
(2)饿汉模式 无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。 由静态初始化实例保证其线程安全性。因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化。不必担心多线程问题。 故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
class Singleton{ private: static const Singleton* instance; Singleton(){}; public: static const Singleton* GetInstance(){ return instance; } }; const Singleton* Singleton::instance=new Singleton();静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类。 之前的处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。
已知类String的原型为:
class String{ public: String(const char *str=NULL); //普通构造函数 String(const String &other); //拷贝构造函数 ~String(void); //析构函数 String &operator = (const String &other); //赋值函数 private: char *m_data; //用于保存字符串 }解答: //普通构造函数
String::String(const char *str=NULL){ if(str == NULL){ m_data = new char[1]; //得分点,对空字符串自动申请存放结束标志'\0' *m_data='\0'; }else{ int length = strlen(str); m_data = new char[length+1]; strcpy(m_data, str); } }//String的析构函数
String::~String(void) { delete[] m_data; }//拷贝构造函数
String::String(const String &other) { //得分点,输入参数为const型 int length = strlen(other.m_data); m_data = new char[length + 1]; //加分点,对other.m_data做NULL判断 strcpy(m_data, other.m_data); }//赋值函数
String & String::operator=(const String &other) { if(this == &other){ return *this; } delete[] m_data; //得分点,释放原有的内存资源 int length = strlen(other.m_data); m_data = new char[length+1]; //加分点,对m_data加NULL判断 strcpy(m_data, other.m_data); return *this; //得分点,返回本对象的引用 }本质上:引用是别名,指针是地址 具体的: (1)引用在创建的同时必须被初始化,不能有NULL引用,引用只能在定义时被初始化一次,之后不可变。指针可以在运行时改变其指向的值。 (2)从内存上看,指针是一个实体,指针会分配内存区域。而引用不会,它仅仅是一个别名。 (3)引用不能为空,指针可以为空。 (4)引用没有const,指针有const,const的指针不可变。 (5)“sizeof引用”得到的是所指向的变量(对象)的大小,而“sizeof指针”得到的是指针本身的大小。 (6)指针和引用的自增(++)运算意义不一样。
例子:
int a = 0; int &b = a; //b是a的一个引用,b只是a的一个别名,和a一样使用。 int *p = &a; //p是a的一个指针 cout<<b<<endl; b++; cout<<b<<endl; cout<<p<<endl; p++; cout<<p<<endl;输出: 0 1 0x7fff59fdeb98 0x7fff59fdeb9c
关于引用: 引用:就是某一个变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。 引用的声明方法:类型标识符 &引用名 = 目标变量名 例如引用a: int a; int &ra = a; 注意:(1)&在此不是求地址运算符,而是起标识作用 (2)类型标识符是指目标变量的类型 (3)声明引用时,必须同时对其进行初始化 (4)引用声明完毕后,相当于目标变量有两个名称即该目标原名称和引用名,且不能再把该引用作为其他变量的别名。 (5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址,&ra与&a相等。 (6)不能建立数组的引用,因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
连接数据库大致分如下四个步骤: (1)初始化 (2)用Connection对象连接数据库 (3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理 (4)使用完毕后关闭连接释放对象
模板是c++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。 模板是一种对类型进行参数化的工具 通常有两种形式:函数模板和类模板。
函数模板针对仅参数类型不同的函数 类模板针对仅数据成员和成员函数类型不同的类
使用模板的目的就是能够让程序员编写与类型无关的代码。
注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
(1)函数模板 template < class 形参名, class 形参名,….. > 返回类型 函数名(参数列表) { 函数体 }
例子: 比如swap的模板函数形式为: template < class T > void swap(T&a, T&b){}; 当调用这样的模板函数时类型T就会被调用时的类型所代替,比如swap(a, b)其中a和b是int 型,这时模板函数swap中的形参T就会被int 所代替,模板函数就变为swap(int &a, int &b)。而当swap(c, d)其中c和d是double类型时,模板函数就会被替换为swap(double &a, double &b),这样就实现了函数的实现与类型无关的代码。
(2)类模板 template < class 形参名, class 形参名, … > class 类名 { … };
一旦声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。
例子: template < class T > class A { public : T a; T b; T hy(T c, T &d); }; 在类A中声明了两个类型为T的成员变量a和b,还声明了一个返回类型为T带两个参数类型为T的函数hy。
(1)申请的内存所在位置 new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。 自由存储区是c++基于new操作符的一个抽象概念。自由存储区可以是堆,也可以是静态存储区,这都看operator new在哪里为对象分配内存。
(2)返回类型安全性 new操作符内存分配成功时,返回的是对象类型的指针。类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。malloc内存分配成功则是返回void ,需要通过强制类型转换将void 指针转换成我们需要的类型。
(3)内存分配失败时的返回值 new内存分配失败时,会抛出bad_alloc异常,它不会返回NULL,malloc分配内存失败时返回NULL。
(4)是否需要指定内存大小 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。 malloc则需要显式地指出所需的尺寸。 例子: A *ptr = new A; A ptr = (A*)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A)
(5)是否调用构造函数/析构函数 new/delete会调用对象的构造函数/析构函数从完成对象的构造/析构。malloc则不会调用。
(6)对数组的处理 c++提供了new[]与delete[]来专门处理数组类型。使用new[]分配的内存必须使用delete[]进行释放,两个要配套使用,不然会出现数组对象部分释放的现象,造成内存泄漏。 malloc动态分配一个数组的内存,需要手动自定数组的大小。 int ptr = (int )malloc(sizeof(int)*10); //分配一个10个int元素的数组
(7)new与malloc是否可以相互调用 operator new/operator delete的实现可以基于malloc。而malloc的实现不可以去调用new。
(8)是否可以被重载 operator new/operator delete可以被重载。malloc/free并不允许重载
(9)能够直观地重新分配内存 使用malloc分配内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。new没有这样直观的配套设施来扩充内存。
(10)客户处理内存分配不足 在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new_handler。对于malloc,客户并不能够去编程决定内存不足以分配时要干什么,只能看着malloc返回NULL。
头文件(.h) 写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。 源文件(.cpp) 主要写实现头文件中已经声明的那些函数的具体代码。需要注意的是,开头必须#include一下实现的头文件,以及要用到的头文件。那么当你需要用到自己写的头文件中的类时,只需要#include进来就行了。
(1)作用:防止该头文件被重复引用。 “被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:a.h文件#include“c.h”,而此时b.cpp文件导入了#include “a.h”和#include“c.h”,此时就会造成c.h重复使用。
(2)头文件被重复引用引起的后果。 有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率低下那将是一件多么痛苦的事情。 有些头文件重复包含,会引起错误。比如在头文件中定义全局变量(虽然这种方式不被推荐,但确实是c规范允许的)这种会引起重复定义。
(3)是不是所有的头文件中都要加入#ifndef、#define、#endif这些代码? 不一定要加,但是不管怎样,用ifndef…或者其他方式避免头文件重复包含,只有好处没有坏处。 例子:
#ifndef GRAPHICS_H //防止 graphics.h被重复引用 #define GRAPHICS_H #include<math.h> //引用标准库的头文件 #include“header.h” //引用非标准库的头文件 ... void Function(...); //全局函数声明 ... class Box{ //类结构声明 ... }; #endif编译一般分为四个步骤: 预处理->编译->汇编->链接 gcc认为预处理的文件是c文件,并且设定c形式的连接 g++认为预处理的文件是c++文件,并且设定c++形式的连接
(1)编译预处理 预处理过程主要处理那些源文件中的以”#”开始的预编译指令,主要处理规则有: 1. 将所有的”#define”删除,并展开所用的宏定义 2. 处理所有条件预编译指令,比如“#if”、“#ifndef”、“#elif”、“#endif”。 3. 处理”#include”预编译指令,将所包含的文件插入到该编译指令的位置,注:此过程是递归进行的。 4. 删除所有注释 5. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时可显示行号。 6. 保留所有的#pragma编译器指令。(pragma的作用是设定编译器的状态或者是指示编译器完成一些特定的动作)
(2)编译 编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程是整个程序构建的核心部分。 词法分析->语法分析->语义分析->中间代码生成->代码优化->目标代码生成->符号表管理->…
(3)汇编 汇编器是将汇编代码转化为机器可以执行的指令,每一条汇编语句几乎都是一条机器执行。经过编译、链接、汇编输出的文件称为目标文件。
(4)链接 链接的主要内容就是把各个模块之间相互引用的部分处理好,使各个模块可以正确的拼接。链接的主要过程包括地址和空间的分配、符号决议和重定位等步骤。
静态链接和动态链接的区别: 静态库里的代码在编译期就会嵌入可执行文件。 动态库在程序运行时,动态加载到内存,或者按照需要,随时被加载到内存。
动态库链接时,会在运行时选择需要的部分进行编译,生成的可执行文件会比较小,而且容易后续的更新。 静态编译会把所有的函数等都嵌入到可执行文件中,可以直接在任何电脑上直接运行,同样文件会远远大于动态链接的。
声明:是用以告诉编译器类型及其细节 例如: class MyClass{ //数据成员细节 //成员函数细节 } 上述声明仅告诉编译器有自定义类型MyClass,编译器仅对其进行语汇分析及名字的决议,并未占用内存。
定义:即内存占有。编译器将在相对内存地址上为其对象定址。 注意:我们不能简单的说string mystring是声明还是定义,判断的原则是看是否占用内存。
总结: (1)变量和对象不加extern永远是定义,类中的除外 (2)函数只有函数头是声明,有函数体的是定义 (3)类永远只是声明,类成员函数的函数体是定义
例子:
class Myclass{ static int x; //这里的x是声明 static const int a; //这里的a是声明 //非static变量在类实例化时才分配内存 Myclass(); //这里的函数是声明 } int Myclass::x; //这是定义 const int Myclass::a = 11; //这是定义map、set属于标准关联容器,使用了非常高效的平衡检索二叉树:红黑树。它的插入删除效率比其他序列容器高是因为不需要做内存拷贝和内存移动,而直接替换指向结点的指针即可。
set是一种关联式容器,其特性如下: (1)set以红黑树(RBTree)作为底层容器 (2)所有元素只有key没有value,value就是key (3)不允许出现键值重复 (4)所有的元素都会被自动排序 (5)不能通过迭代器来改变set的值,因为set的值就是键。(STL中将set的迭代器设置为const,不允许修改迭代器的值)。对于set只能insert和delete
map和set一样是关联式容器,它们的底层容器都是红黑树,区别就在于map的值不作为键,键和值是分开的,它的特性如下: (1)map以红黑树(RBTree)作为底层容器 (2)所有元素都是键+值存在 (3)不允许键重复 (4)所有元素是通过键进行自动排序的 (5)map的键是不能修改的,但是其键对应的值是可以修改的。
红黑树的5个性质: (1)每个结点要么是红的要么是黑的; (2)根结点是黑的; (3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的; (4)如果一个结点是红的,那么它的两个儿子都是黑的; (5)对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点;
(1)vector底层数据结构为数组,它拥有一段连续的内存空间,并且起始地址不变。支持快速随机访问,可以使用[]访问任意元素。vector每次扩容为原来的两倍,对小对象来说执行效率高,但如果遇到大对象,执行效率就低了。可以快速地在最后添加删除元素。
(2)list底层数据结构为双向链表,因此它的内存空间可以是不连续的,因此只能通过指针来进行数据的访问。可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素。
(3)deque:底层数据结构为一个中央控制器和多个缓冲区。支持首尾(中间不能)快速增删,也支持随机访问。deque是一个双端队列,也是在堆中保存内容的。它的保存形式如下: [堆1]->[堆2]->[堆3]->… 每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品。 deque支持[]操作符,也就是支持随机存取,而且跟vector的效率相差无几,它支持两端的操作:push_back,push_front,pop_back,pop_front等。并且在两端操作上与list的效率也差不多。因此可以认为:deque是vector和list的折中。
总结: (1)如果你需要高效的随机存取,而不在乎插入和删除的效率,使用vector (2)如果你需要大量的插入和删除,而不关心随机存取,则应使用list (3)如果你需要随机存取,而且关系两端数据的插入和删除,则应使用deque
vector::iterator和list::iterator的区别? 由于vector拥有一段连续的内存空间,能非常好的支持随机存取,因此vector::iterator支持“+”“+=”“<”等操作符。 而list的内存空间可以是不连续的,它不支持随机访问,因此list::iterator不支持“+”“+=”“<”等操作符运算,只能用“++”进行迭代。
(1)重载(overload) 同一作用域中函数名相同,参数列表不同的函数,叫做重载。 基本条件:1、同一作用域。2、函数名相同。3、函数参数必须不相同(具体的参数列表不同表现在三个方面:参数类型不一样,参数个数不一样,参数顺序不一样,只有满足上述三个条件的一条或一条以上,才算做参数列表不同)。 注意:函数返回值可以相同,也可以不相同。
(2)重写(override) 重写也称为覆盖,子类重新定义父类中有相同名称和参数的虚函数,主要在继承关系中出现。 基本条件: 1、派生类中重写的函数和基类中被重写的函数必须为virtual函数 2、重写的函数和被重写的函数函数名和函数参数必须一致 3、重写的函数和被重写的函数返回值相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的类型是基类中被替换的虚函数所返回的指针或引用的类型的子类型。 注意: (1)重写的函数所抛出的异常必须和被重写的函数所抛出的异常一致,或者是其子类。 (2)重写的函数的访问修饰符可以不同于被重写的函数,如基类的virtual函数的修饰符为private,派生类改为public或protected也是可以的。 (3)静态方法不能被重写,也就是static和virtual不能同时使用。 (4)重写的函数可以带virtual关键字,也可以不带。
(3)重定义(redefining) 也叫隐藏,子类重新定义父类中的非虚函数,屏蔽了父类的同名函数。
基本条件:被隐藏的函数之间作用域不相同。
注意: 1、子类和父类的函数名称相同,但参数不同,此时不管父类函数是不是virtual函数,都将被隐藏。(与重载的区别:重载要求在同一作用域内,而重定义是指父类和子类中) 2、子类和父类的函数名称相同,参数也相同,但是父类函数不是virtual函数,父类的函数将被隐藏。
两者都可以定义常量,区别在于: (1)define是简单的文本替换,是在编译预处理进行,const是在编译时进行,const是分配内存的。const占用一份内存,define不占用内存但是会多处进行字符串替换。 (2)define不做类型检查,const可以进行类型的检查 (3)define可以定义一些简单的函数,const不可以 (4)define定义的常量不能调试,const可以进行调试 (5)const避免不要的内存分配,而且效率更高。define定义的常量在替换后运行过程中会不断地占用内存,在内存中有若干份copy,而const定义的常量存储在数据段,只有一份copy,效率更高。
面向过程就是自顶向下逐步编程,分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。其最重要的是模块化的思想方法。 面向对象的方法主要是把事务给对象化,包括其属性和行为,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。面向对象就是你把要做的事情抽象成对象,告诉对象去做。面向对象三大特性(封装、继承、多态)使得在做复杂的事情的时候效率和正确率得到保证。
三大特性: 封装:把客观事物封装成抽象的类 继承:让某个类型的对象获得另一个类型的对象的属性的方法 多态:一个类实例的相同方法在不同情形有不同表现形式
(1)浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象,在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B,这时,如果B中有一个成员变量指针已经申请了内存,那A种的那个成员变量也指向同一块内存,这就出现了问题。当B把内存释放了(如析构),这时A内的指针就是野指针了,出现运行错误。
(2)深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。 浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
多态性可以简单地概括为“一个接口,多种方法”。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。 c++多态性是通过虚函数来实现的,只有重写了虚函数的才能算作是体现了c++多态性。多态的目的是为了接口重用,不论传递过来的究竟是哪个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
例子:
#include <iostream> using namespace std; class animal { public: void sleep() { cout<<"animal sleep"<<endl; } void breathe() { cout<<"animal breathe"<<endl; } }; class fish:public animal { public: void breathe() { cout<<"fish bubble"<<endl; } }; int main() { fish fh; animal *pAn=&fh; // 隐式类型转换 pAn->breathe(); return 0; }输出结果: animal breathe
将breathe函数改为虚函数
#include <iostream> using namespace std; class animal { public: void sleep() { cout<<"animal sleep"<<endl; } virtual void breathe() { cout<<"animal breathe"<<endl; } }; class fish:public animal { public: void breathe() { cout<<"fish bubble"<<endl; } }; int main() { fish fh; animal *pAn=&fh; // 隐式类型转换 pAn->breathe(); return 0; }输出结果: fish bubble
虚函数更多总结见博文:虚函数总结
(1)c中的struct和c++的class区别? c是一种过程化的语言,struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数。(c++中的struct可以包含成员函数,可以实现多态和继承)。 (2)c++中的struct和class的区别? 访问权限上:class中默认的成员访问权限是private,而struct中则是public。 继承上:class继承默认是private继承,而struct继承默认是public继承。 其他:class这个关键字还用于定义模板参数,就像typename,但关键字struct不用于定义模板参数。
STL中的遍历可以是以下两种之一 for(iterator it=begin();it!=end();++it){return it->second;} for(iterator it=begin();it!=end();it++){return it->second;} 每次返回的结果是否相同? 答:两种方式iterator遍历的次数是相同的,但在STL中效率不同,前++返回引用,后++返回一个临时对象,因为iterator是类模板,使用it++这种形式要返回一个无用的临时对象,而it++是函数重载,所以编译器无法对其进行优化,所以每遍历一个对象,你就创建并销毁了一个无用的临时对象。除了特殊需要和对内置类型外,基本都是使用++it来进行元素遍历的。
for循环中i++和++i的区别? 1、++i的用法(以a=++i, i=2为例) 先将i值加1(也就是i = i+1),然后赋给变量a(也就是a = i) 则最终a值等于3,i值等于3 所以a = ++i 相当于i = i+1, a = i 2、i++的用法(以a = i++,i=2为例) 先将i值赋给变量a(也就是a = i),然后i值加1(也就是i = i + 1) 则最终a值等于2, i值等于3 所以a = i++,相当于a = i, i = i + 1 3、++i 与 i++单独使用时,相当于 i = i+1 如果赋给一个新变量,则++i先将i值加1,而i++先将i赋给新变量。
i ++ :先引用后增加 ++ i : 先增加后引用
(1)const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。 (2)在c++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化。 在c++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
const总结: 1、指针常量:constant pointers //指针本身是个常量 int* const p = &a; //指针p的值是不可以修改的 所以指针常量必须在声明时初始化,指定一个地址的值,即必须指向一个变量,指针常量的值不能修改,就是说不能更换成一个新的地址,不能指向别的常量,但是可以通过指针常量修改它所指向的变量的值。
2、常量指针:pointer to constants //一个指针指向一个常量或者指向一个普通变量是一个常量 const int *p //指针p的值可以修改,就是说可以指向另外的常量 常量指针不能通过指针修改所指向的常量,但是指针可以指向别的常量,因为指针p本身是变量,但它所指向的是常量。
3、函数参数为const int func(const int&) 函数体内不能修改参数的值
4、参数返回值为const引用 const int &func(int) 函数返回值为const引用,即函数值不能被修改,将const赋给另一个const引用才有意义。
5、 常数据成员变量 const int age; //age是一个成员变量 常数据成员的值不能改变,只能通过构造函数的初始化列表对常数据成员变量进行初始化,而不能采用一般的在构造函数中赋值的方法实现。 比如 CStudent::CStudent(int a):age(a){} CStudent::CStudent(int a){age = a} //这种是错误的
6、常成员函数 void print() const; //print()是一个成员函数 常成员函数只能访问本类的数据成员(包括const和非const数据成员),但是不能修改他们的值。只有常成员函数才可以操作常量或者常对象。在实现时加上const关键字,在调用时不必加const。
7、常对象:const 类名 对象名[()] 例如const Cdate d1(2008,8,8) d1就是常对象,对象d1中的所有成员的值都不能被修改,定义常对象必须要有初值,不能做左值,如果定义了常对象,则不能调用该对象的非const成员函数,只能访问const成员函数,如果希望成员函数访问常对象,只需要将该成员函数声明为const即可。
static总结 static的作用: (1)隐藏 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。举个例子,我们要同时编译两个源文件,一个是a.c,另一个是b.c。其中a.c的内容如下:
#include<cstdio> char a = ‘A’; void msg() { printf(“hello\n”); }其中a和msg未加static前缀,所以是具有全局可见性,其他的源文件也能访问。所以在b.c中a和msg也是可见的。 如果加了static,就会对其他源文件隐藏。例如在a和msg()的定义前加上static,b.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来说,static的作用仅限于隐藏,而对于变量,static还有下面两个作用: (2)保持变量内容的持久,存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。 (3)默认初始化为0,其实全局变量也具备这一属性,因为全局变量也存储在静态存储区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
最后对static的三条作用做一句话总结:首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值为0。
静态局部变量的特点: (1)该变量在全局数据区分配内存(局部变量在栈区分配内存) (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化(局部变量每次函数调用都会被初始化) (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化) (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体使用它(局部变量在栈区,在函数结束后立即释放内存)
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高。
inline和宏的区别? (1)宏不可调试,但是内联函数可以调试 (2)宏(define)不做类型检查,内联函数要做参数类型检查 (3)宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。在编译的时候,内联函数可以直接被镶嵌到目标代码中。 例如: inline void Foo(int x, int y); //inline仅与函数声明放在一起,不能成为内联函数 void Foo(int x, int y){} 而下面的函数Foo则称为内联函数 void Foo(int x, int y); inline void Foo(int x, int y){}; //inline与函数定义体放在一起
inline一般只用于如下情况: (1)一个函数不断被重复调用 (2)函数只有简单的几行,且函数内不包含for,while,switch语句
以下情况不宜使用内联: (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高 (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
sizeof一个空类的大小是1,因为一个空类也要实例化,所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。
在这个类中添加一个virtual函数后再sizeof,这时是多大?为什么? 加入虚函数后需要维护一个虚函数表,所以要占据一个指针大小,也就是8字节,此时sizeof是8(对于64位机器)
总结: (1)类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑(因为静态变量是存放在全局数据区的,而sizeof计算栈中分配的大小) (2)普通成员函数与sizeof无关 (3)虚函数由于要维护在虚函数表中,所以要占据一个指针大小,32位机器也就是4字节,对于64位机器是8字节 (4)类的总大小也遵循类似class字节对齐的调整原则
举个例子(在64位机器中)
#include<iostream> using namespace std; class empty{ empty(); ~empty(); }; class Base{ public: Base(); virtual ~Base(); //虚函数表指针占8字节 void print_num(){ //普通成员函数,不归入sizeof统计 cout<<"hello"<<endl; } private: int a; //占4个字节,但是要跟最大的字节对齐,因此会填充为8字节 char *p; //指针都占8字节 }; class Derive:public Base{ public: Derive():Base(){}; ~Derive(); private: static int st; //不占 int d; //占4个字节,填充为8个字节 char *p; //指针都占8个字节 }; int main(){ cout<<sizeof(empty)<<endl; //1个字节 cout<<sizeof(Base)<<endl; //24个字节 cout<<sizeof(Derive)<<endl; //基类的24个字节+d的8个字节+p的8个字节=40个字节 return 0; }输出: 1 24 40
权限有private,public,protected (1)访问标号的访问范围 private:只能由1、该类中的函数 2、其友元函数访问 不能被任何其他访问,该类的对象也不能访问
protected:只能由1、该类中的函数 2、其友元函数访问 3、子类中的函数 但不能被该类的对象访问
public:可以被1、该类中的函数 2、其友元函数访问 3、子类中的函数 4、该类的对象访问
(2)类的继承后方法属性变化 private属性不能够被继承 使用private继承,父类的protected和public属性在子类中变为private 使用protected继承,父类的protected和public属性在子类中变为protected 使用public继承,父类的protected和public属性不发生改变
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。 友元可以是一个函数,该函数被称为友元函数。友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
例子:
class Box{ double width; public: friend void printWidth(Box box); void setWidth(double wid); }; //成员函数定义 void Box::setWidth(double wid){ width = wid; } //print width()不是任何类的成员函数,因为它是Box的友元,它可以直接访问该类的任何成员 void printWidth(Box box){ cout<<“Width of box:”<<box.width<<endl; } class B{ public: B(int b):b(_b){}; friend class c; //声明友元类c private: int b; } class c{ public: int getB_b(B _classB){ return _classB.b; //访问友元类B的私有成员 } }注意: (1)友元关系没有继承性 假如类B是类A的友元,类C继承于类A,但是友元类B是没法直接访问类C的私有或保护成员。 (2)友元关系没有传递性 假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
(1)指针 java没有指针,虚拟机内部还是使用了指针,只是外人不得使用而已。这有利于java程序的安全。 (2)多重继承 c++支持多重继承,它允许多父类派生一个类。尽管多重继承功能很强,但使用复杂,而且会引起许多麻烦,编译程序实现它也很不容易。java不支持多重继承,但允许一个类继承多个接口,实现了c++多重继承的功能,又避免了c++中的多重继承实现方式带来的诸多不便。 (3)自动内存管理 java自动进行无用内存回收操作,不需要程序员进行删除。而c++中必须由程序员释放内存资源,增加了程序设计者的负担。java中当一个对象不被再用到时,无用内存回收器将给它加上标签以示删除。java里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作。 (4)操作符重载 java不支持操作符重载。操作符重载被认为是c++的突出特征。 (5)预处理功能 java不支持预处理功能。c/c++在编译过程中都有一个预编译阶段,即预处理器。java虚拟机没有预处理器,但它提供的引入语句(import)与c++预处理器的功能类似。 (6)java不支持缺省函数参数,而c++支持 (7)类型转换 java不支持c++中的自动强制类型转换,如果需要,必须由程序显示进行强制类型转换。 (8)数据类型及类 java是完全面向对象的语言,所有函数和变量都必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而c++允许将函数和变量定义为全局的,此外,java中取消了c/c++中的结构和联合。
1、结构体的定义: struct 结构体名{ 数据类型1 成员名1; 数据类型2 成员名2; … }; 2、联合体的定义: union 联合体名{ 数据类型1 成员名1; 数据类型2 成员名2; } 多种变量共用一个存储空间,以达到节省空间的作用
区别: (1)在同一时刻,结构体的每个成员都有值,但是联合体在同一时刻只有一个成员有值(或理解为结构体的sizeof是所有成员的和,而联合体的sizeof等于其最长的成员的sizeof) (2)当对结构体变量的其中一个成员进行修改时,对其他成员没有影响,但是修改联合体时,则会将原来的成员值覆盖。
例子:
int main(){ union test{ int i; struct{ char first; char second; }half; }number; number.i = 16961; cout<<number.i<<endl; cout<<number.half.first<<endl; cout<<number.half.second<<endl; number.half.first = 'a'; cout<<number.i<<endl; cout<<number.half.first<<endl; cout<<number.half.second<<endl; return 0; }输出: 16961 A B 16993 a B
(1)c面向过程化,c++面向对象(但c++并非完全面向对象化,真正的面向对象化的语言恐怕只有java才算的上) (2)c语言不支持函数重载,c++语言支持函数重载 (3)在c程序中如果在声明函数的时候没有任何参数那么需要将参数定义为void,以此来限定此函数不可传递任何参数。如果不进行限定让参数表默认为空,其意义是可以传递任何参数。而c++标准规定如果没有对参数列表进行定义那么就表示函数不能传递任何参数。所以如果参数中是void,那么不管在c还是c++中都表示不可以传递任何参数。
不能,有纯虚函数的类是抽象类,只能被继承,不能实例化。包含纯虚函数的类派生出来的类都必须重写这个纯虚函数。 纯虚函数形式为:virtual void foo()=0;
虚函数更多总结见博文:虚函数总结
在c/c++中,数组和指针是相互关联又有区别的两个概念。当我们声明一个数组时,其数组的名字也是一个指针,该指针指向数组的第一个元素。我们可以用一个指针来访问数组。但是值得注意的是,c/c++没有记录数组的大小,因此用指针访问数组中的元素时,程序员要确保没有超出数组的边界。下面通过一个例子来了解数组和指针的区别。
代码:
#include<iostream> using namespace std; int GetSize(int data[]){ return sizeof(data); } int main(){ int data1[] = {1, 2, 3, 4, 5}; int size1 = sizeof(data1); int* data2 = data1; int size2 = sizeof(data2); int size3 = GetSize(data1); cout<<size1<<" "<<size2<<" "<<size3<<endl; return 0; }输出:
20 8 8data1是一个数组,sizeof(data1)是求数组的大小。这个数组包含5个整数,每个整数占4个字节,因此总共是20字节。data2声明为指针,尽管它指向了数组data1的第一个数字,但它的本质仍然是个指针。在64位系统上,对任意指针求sizeof,得到的结果都是4。下面重点来了,为什么最后一个输出也是4呢。 在c/c++中,当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针,因此尽管函数GetSize的参数data被声明为数组,但它会退化为指针,sizeof的结果仍是8。
下面再举另外一个例子,代码如下:
char str1[] = "hello world"; char str2[] = "hello world"; char* str3 = "hello world"; char* str4 = "hello world"; if(str1 == str2){ cout<<"str1 and str2 are same."<<endl; }else{ cout<<"str1 and str2 are not same."<<endl; } if(str3 == str4){ cout<<"str3 and str4 are same."<<endl; }else{ cout<<"str3 and str4 are not same."<<endl; } return 0;输出:
str1 and str2 are not same. str3 and str4 are same.分析: str1和str2是两个字符串数组,我们会为它们分配两个长度为12个字节的空间,并把“hello world”的内容分别复制到数组中去。这是两个初始地址不同的数组,因此str1和str2的值也不相同,所以输出的第一行是“str1 and str2 are not same.”
str3和str4是两个指针,我们无须为它们分配内存以存储字符串的内容,而只需要把它们指向“hello world”在内存中的地址就可以了。由于“hello world”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。所以比较str3和str4的值得到的结果是相同的,输出的第二行是“str3 and str4 are same.”
二者的区别和联系 (1)误区:gcc只能编译c代码,g++只能编译c++代码。 事实上两者都可以,但要注意一下几点: 1、后缀为.c的,gcc把它当做是c程序,而g++当做是c++程序;后缀为.cpp的,两者都会认为是c++程序。虽然c++是c的超集,但是两者对语法的要求是有区别的。例如:
#include<stdio.h> int main() { int a = 2; char* argv = "hello"; if(a == 0) return; printString(argv); return; } int printString(char* string){ sprintf(string, "This is a test.\n"); }按照c语言的语法规则,是没有问题的,但一旦把后缀改为cpp或者用g++编译,将其当做一个c++程序,会立即报错。可见c++的语法规则更加严谨一些。
# gcc test.c # g++ test.c test.c: In function ‘int main()’: test.c:5:15: warning: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings] char* argv = "hello"; ^ test.c:6:13: error: return-statement with no value, in function returning ‘int’ [-fpermissive] if(a == 0) return; ^ test.c:7:18: error: ‘printString’ was not declared in this scope printString(argv); ^ test.c:8:2: error: return-statement with no value, in function returning ‘int’ [-fpermissive] return; ^(2)编译阶段,g++会调用gcc,对于c++代码,两者是等价的;但是因为gcc命令不能自动和c++程序使用的库链接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接统一用g++了,所以会有人误以为cpp程序只能用g++。
(3)误区二:编译只能用gcc,链接只能用g++ 严格来说,这句话不算错误,但是它混淆了概念。应该这样说:编译可以用gcc/g++,而链接可以用g++或者gcc-lstdc++。因为gcc命令不能自动和c++程序使用的库链接,所以通常使用g++来完成链接。但是在编译阶段,g++会自动调用gcc,二者等价。
(4)误区三:gcc不会定义__cplusplus宏,而g++会 实际上,这个宏只是标志着编译器将会把代码按c还是c++语法来解释,如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的,否则就是已定义。
(5)误区四:extern”c” 与gcc/g++有关系 实际上并无关系,无论是gcc还是g++,用extern”c”时,都是以C的命令方式来为symbol命名;否则,都以c++方式命令。
指针数组:用于存储指针的数组,也就是数组元素都是指针 数组指针:指向数组的指针 int* a[4]:指针数组,表示:数组a中的元素都为int型指针,元素表示:a[i] (a[i])是一样的,因为[]优先级高于* Int (*a)[4]:数组指针,表示:指向数组a的指针,元素表示:(*a)[i]
例子:
#include<iostream> using namespace std; int main(){ int c[4] = {1, 2, 3, 4}; int *a[4]; //指针数组 int (*b)[4]; //数组指针 b = &c; for(int i=0;i<4;i++){ a[i] = &c[i]; } cout<<*a[1]<<endl; cout<<(*b)[2]<<endl; return 0; } 输出: 2 3vector空间的动态增长 当添加元素时,如果vector空间大小不足,则会以原大小的两倍另外配置一块较大的新空间,然后将原空间内容拷贝过来,在新空间的内容末尾添加元素,并释放原空间。vector的空间动态增加大小,并不是在原空间之后的相邻地址增加新空间,因为vector的空间是线性连续分配的,不能保证原空间之后有可供配置的空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就会失效。
一个验证的例子:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> nums; cout<<"nums.size():"<<nums.size()<<" "<<"nums.capacity():"<<nums.capacity()<<endl; for(int i=0;i<20;i++){ nums.push_back(i); cout<<"nums.size():"<<nums.size()<<" "<<"nums.capacity():"<<nums.capacity()<<endl; } return 0; }输出结果
nums.size():0 nums.capacity():0 nums.size():1 nums.capacity():1 nums.size():2 nums.capacity():2 nums.size():3 nums.capacity():4 nums.size():4 nums.capacity():4 nums.size():5 nums.capacity():8 nums.size():6 nums.capacity():8 nums.size():7 nums.capacity():8 nums.size():8 nums.capacity():8 nums.size():9 nums.capacity():16 nums.size():10 nums.capacity():16 nums.size():11 nums.capacity():16 nums.size():12 nums.capacity():16 nums.size():13 nums.capacity():16 nums.size():14 nums.capacity():16 nums.size():15 nums.capacity():16 nums.size():16 nums.capacity():16 nums.size():17 nums.capacity():32 nums.size():18 nums.capacity():32 nums.size():19 nums.capacity():32 nums.size():20 nums.capacity():32讨论到内存大小就不得不提一下size(),capacity(),reserve(),resize()这四个函数 (1)size()函数:返回的是当前vector中已有元素的大小 (2)capacity()函数:返回的是当前vector空间的大小 (3)resize()函数:调整容器的长度大小,使其能容纳n个元素,如果n小于容器的当前的size,则删除多出来的元素。否则添加采用值初始化的元素,例如resize(n, t),如果有新添加的元素都初始化为t,如果没有给出t这个参数,int型的容器默认添加的是0。改变的是size的大小,而不是capacity (4)reserve()函数:预分配n个元素的存储空间,改变的是capacity的值,而不是size。调用reserve(n)后,若容器的capacity < n,则重新分配内存空间,从而使得capacity等于n。如果capacity>=n,capacity无变化。
例子:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> nums; nums.push_back(3); nums.reserve(100); nums.resize(50); for(int i=0;i<10;i++){ cout<<nums[i]<<" "; } cout<<endl; cout<<nums.size()<<" "<<nums.capacity()<<endl; nums.reserve(50); cout<<nums.size()<<" "<<nums.capacity()<<endl; nums.resize(50); cout<<nums.size()<<" "<<nums.capacity()<<endl; return 0; }输出:
3 0 0 0 0 0 0 0 0 0 50 100 50 100 50 100释放vector内存 当vector、string大量插入数据后,即使删除了大量数据(或者全部都删除,即clear)并没有改变容器的容量(capacity),所以仍然会占着内存。为了避免这种情况,我们应该想办法改变容器的容量使之尽可能小的符合当前数据所需。 《effective STL》给出的解决方案是
vector<type> v; //.....这里添加许多元素给v //......这里删除v中的许多元素 vector<type>(v).swap(v); //此时v的容量已经尽可能的符合其当前包含的元素数量 //对于string则可能像下面这样 string(s).swap(s)即先创建一个临时拷贝与原先的vector一致,值得注意的是,此时的拷贝其容量是尽可能小的符合所需数据的。紧接着将该拷贝与原先的vector v进行交换。好了此时,执行交换时,临时变量会被销毁,内存得到释放。此时的v即为原先的临时拷贝,而交换后的临时拷贝则为容量非常大的vector(不过已经被销毁)
一个验证的例子
#include <iostream> #include <vector> using namespace std; vector<string> v; char ch; int main() { for(int i=0;i<1000000;i++){ v.push_back("abcdefghijklmn"); } cout<<v.capacity()<<endl; //此时检查内存 v.erase(v.begin(), v.begin()+999900); cout<<v.capacity()<<endl; vector<string> (v).swap(v); cout<<v.capacity()<<endl; return 0; }输出:
1048576 1048576 100上面这种方法虽然释放了内存,但是同时也增加了拷贝数据的时间消耗,不过一般重新调整容量的情况都是vector本身元素较少的情况,所以时间消耗可以忽略不计。
python常考面试题可以参考博客:http://codingpy.com/article/essential-python-interview-questions/
(1)到底什么是python?通过与其他技术进行对比 1、python是一种解释型语言。c++是编译型语言。python代码在运行之前不需要编译,其他解释型语言还包括php和ruby。 2、python是动态型语言,指的是你在声明变量时,不需要说明变量的类型。你可以直接编写类似x=111和x=“i’m a string”这样的代码,程序不会报错。 3、代码编写风格:python采用缩进,c++是大括号,python没有”;”,c++每句代码后有引号。 4、python代码编写快,但是运行速度比编译语言通常要慢。好在python允许加入基于c语言编写的扩展,因此我们能够优化代码,消除瓶颈,这点通常是可以实现的。numpy就是一个很好的例子,它的运行速度真的非常快,因为很多算术运算其实并不是通过python实现的。
c++风格的类型转换提供了4种类型转换操作符来应对不同场合的应用。 (1)const_cast 作用:去掉类型的const或volatile属性
——- 补充点volatile的知识 ——–
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其他线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
例子:
volatile int i=10; int a = i; ... // 其他代码,并未明确告诉编译器,对 i 进行过操作 int b = i;volatile指出i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发生两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其他程序检测的变量需要加volatile 2、多任务环境下各任务间共享的标志应该加volatile 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义。
(2)static_cast 类似于C风格的强制转换。无条件转换,静态类型转换。用于: 1、基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。(基类和子类之间的动态类型转换建议用dynamic_cast) 2、基本数据类型转换。enum,struct,int,char,float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。 3、把空指针转换成目标类型的空指针 4、把任何类的表达式转换成void类型 5、static_cast不能去掉类型的const、volitale属性(用const_cast)
(3)dynamic_cast 有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL): 1、安全的基类和子类之间转换 2、必须要有虚函数 3、相同基类不同子类之间的交叉转换。但结果是NULL
(4)reinterpret_cast 仅仅重新解释类型,但没有进行二进制的转换: 1、转换的类型必须是一个指针、引用、算术类型、函数指针或成员指针 2、在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。 3、最普通的用途就是在函数指针类型之间进行转换。 4、很难保证移植性。
总结 去const属性用const_cast 基本类型转换用static_cast 多态类之间的类型转换用dynamic_cast 不同类型的指针类型转换用reinterpret_cast
题1:运行下面的代码,输出结果是什么?
#include<iostream> using namespace std; union{ int i; char x[2]; }a; int main(){ a.x[0]=10; a.x[1]=1; cout<<a.i<<endl; return 0; }输出为266 分析: 大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。 小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
a[0] = 00001010, a[1] = 00000001。输出的i是将二个字节作为一个整数看,pc是小端存储,所以看成了x[1]x[0]即00000001 00001010,算出就是256+8+2=266
题2:运行下面的代码,输出是什么?
#include<iostream> using namespace std; int main(){ union{ int i; struct{ char first; char second; }half; }number; number.i = 0x4241; cout<<number.half.first<<endl; cout<<number.half.second<<endl; number.half.first = 'a'; number.half.second = 'b'; cout<<number.i<<endl; return 0; }输出: A B 25185
分析: 0x4241先转化为二进制位:0100 0010 0100 0001,因为PC是小端模式所以,first在低端,second在高端,因为char是一个字节8为,所以first = 0100 0001=65,ASCII为65的是A,second = 0100 0010 = 66,所以是B 同理当first = ‘a’时,a的ASCII是97,即0110 0001,second = ‘b’,b的ASCII是98,即0110 0010,组合在一起就是 0110 0010 0110 0001,所以i等于25185
一、新的关键字 1、auto c++11中引入auto第一种作用是为了自动类型推导 auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作。 例子: auto a; //这样是错误的,auto是通过初始化表达式进行类型推导,如果没有初始化表达式,就无法确定a的类型 所以一定要初始化!!
auto i = 1; auto d = 1.0; auto str = “hello world!”; auto ch = ‘A’ auto func = less<int>(); vector<int> iv; auto ite = iv.begin(); auto p = new foo(); //对自定义类型进行类型推导auto实际上是在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,似乎auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。
2、decltype decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型。 例子:
int x = 3; decltype(x) y = x;3、nullptr 为了解决原来NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0 例子:
void F(int a){ cout<<a<<endl; } void F(int *p){ assert(NULL != p); cout<<p<<endl; } int main() { int *p = nullptr; int *q = NULL; bool equal = (p == q); //equal的值为true,说明p和q都是空指针 int a = nullptr; //编译失败,因为nullptr不能转型为int F(0); //在c++98中编译失败,有二义性;在c++11中调用F(int) F(nullptr); return 0; }4、lambda表达式 创建一个匿名函数并执行。
用于创建并定义匿名的函数对象,以简化编程工作。语法如下 [函数对象参数](操作符重载函数参数)->返回值类型{函数体} [capture] (parameters)->return-type{body} 如果没有参数,空的圆括号()可以省略,返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话,形如: [capture] (parameters){body} 下面举几个lambda函数的例子:
[ ](int x, int y) { return x+y; }. //隐式返回类型 [ ](int& x) { ++x; } //没有return语句 -> lambda函数的返回类型是void [ ]( ) { ++global_x; } //没有参数,仅访问某个全局变量 [ ]{ ++global_x; } // 与上一个相同,省略了()可以像下面这样显示指定返回类型: [ ]( int x, int y ) -> int { int z = x + y; return z; } 在这个例子中创建了一个临时变量z来存储中间值,和普通函数一样,这个中间值不会保存到下次调用。什么也不返回的lambda函数可以省略返回类型,而不需要使用->void形式。
lambda函数可以引用在它之外声明的变量,这些变量的集合叫做一个闭包,闭包被定义在lambda表达式声明中的方括号[ ]内,这个机制允许这些变量被按值或按引用捕获。下面这些例子就是:
[ ] //未定义变量,试图在lambda内使用任何外部变量都是错误的 [x, &y] //x按值捕获,y按引用捕获 [ & ] //用到的任何外部变量都隐式按引用捕获 [ = ] //用到的任何外部变量都隐式按值捕获 [ &, x ] //x显式地按值捕获,其他变量按引用捕获 [ =, &z ] //z按引用捕获,其他变量按值捕获c++11的这种语法,其实就是匿名函数声明之后马上调用(否则的话,如果这个匿名函数既不调用,又不作为闭包传递给其他函数,那么这个匿名函数就没有什么用处)
5、序列for循环
map<string, int> m{{"a", 1}, {"b", 2}, {"c", 3}}; for(auto p:m){ cout<<p.first<<":"<<p.second<<endl; }在c++中for循环可以使用类似java的简化的for循环,可以用于遍历数组,容器,string以及由begin和end函数定义的序列(即有iterator)
6、变长参数的模板 c++中的pair是一个包含两种不同类型的容器,使用make_pair可构造。例如: auto p = make_pair(1, “c++11”);
c++11引入了变长参数模板,发明了新的数据类型:tuple,tuple是一个N元组,可以传入1个,2个甚至多个不同类型的数据。
auto t1 = make_tuple(1, 2.0, "c++11"); auto t2 = make_tuple(1, 2.0, "c++11", {1, 0, 2});另一个例子体现在print中,在c++11中,我们可以用变长参数模板实现更简洁的print
template<typename head, typename... tail> void Print(Head head, typename... tail){ cout<<head<<endl; Print(tail...); }Print中可以传入多个不同种类的参数,如下: Print(1, 1.0, “c++11”);
7、更加优雅的初始化方法 在引入c++11之前,只有数组能使用初始化列表,其他容器想要使用初始化列表,只能用以下方法:
int arr[3] = {1, 2, 3}; vector<int> v(arr, arr+3);在c++11中,我们可以使用以下语法来进行替换:
int arr[3] = {1, 2, 3}; vector<int> iv(1, 2, 3); map<int, string>{{1, "a"}, {2, "b"}}; string str{"Hello World"};__foo__(开头和结尾都是双下划线):一种约定,python内部的名字,用来区别其他用户自定义的命名,以防冲突。 例如:__init__() :代表类的构造函数,__dict__:类的属性,__doc__:类的文档字符串,__name__:类名
__foo (双下划线开头):可以声明类的私有属性和类的私有方法 例如:__private_attrs:声明该属性为类的私有属性,不能在类的外部被使用或直接访问。在类内部的方法中使用时:self.__private_attrs __private_method:声明该方法为私有方法,不能在类的外部调用。在类内部调用时:self.__private_methods
_foo(单下划线开头):以单下划线开头的表示的是protected类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于from module import *
(1)python的字典通过散列表(即哈希表)实现,搜索的时间复杂度为o(1),c++的STL的map是用红黑树来实现的,搜索的时间复杂度为o(log2n) (2)通过开放定址法处理冲突(c++的STL的unordered_map也是用哈希表实现,通过链地址法处理冲突)
开放定址法在发生冲突时,通过探测函数继续寻找可用的结点,直到找到可用的为止,删除结点时,不能直接去除,而只能做标记(伪删除)。否则会打断所在的探测链,使得其后面的探测链结点不能被索引到。
每个结点entry的结构如下: typdef struct{ Py_ssize_t me_hash; //存储的是me_key的散列值 Pyobject *me_key; Pyobject *me_value; }PyDictEntry; 每个entry都有三种状态 Unused:me_key == NULL && me_value == NULL Active:me_key != NULL && me_key != dummy && me_value != NULL Dummy:me_key == dummy && me_value == NULL
python的PyDictObject(即dict)没有如map一样采用平衡二叉树,而是采用了散列表,因为理论上,在最优情况下,散列表能提供o(1)复杂度的搜索效率。
拉链法:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针,在拉链法中,装填因子可以大于1,但一般均取<=1。 装填因子 = 表中填入的记录数/哈希表的长度
开放地址法:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。 这个过程可用下式描述:Hi(key) = (H(key)+di) mod m (i=1,2,…k(k<=m-1)) 其中:H(key)为关键字key的直接哈希地址,m为哈希表的长度,di为每次再探测时的地址增量 采用这种方法时,首先计算出元素的直接哈希地址H(key),如果该存储单元已被其他元素占用,则继续查看地址为H(key)+d2的存储单元,如此重复直至找到某个存储单元为空时,将关键字为key的数据元素存放到该单元。 增量di有不同的取法: (1)线性探测再散列,di=1,2,3,…. (2)二次探测再散列,di=12,-12,22,-22,…,k2,-k2(k<=m/2) (3)伪随机探测再散列
与开放地址法相比拉链法的优点: (1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短。 (2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。 (3)开放地址法为减少冲突,要求装填因子较小,故当结点规模较大时会浪费很多空间。而拉链法中可取>=1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间。 (4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径。因此只能在被删结点上做删除标记,而不能真正删除结点。
list和tuple的不同首先是语法,相同点:都是类似数组的容器,都可以保存任意类型的对象,不同是tuple中的元素不可变,list中的元素是可以改变的,因此list比tuple多了一些管理其中保存元素的方法。如append,insert,pop,remove,sort,reverse等。
使用中最大的区别是在内存上,range(5)生成的是一个list对象[0,1,2,3,4],而xrange(5)生成的是一个生成器,每次调用返回其中的一个值,因此,要生成很大的数字序列的时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间。
string是字符串,而unicode是一个统一的字符集,utf-8是它的一种存储实现形式,string可为utf-8编码,也可编码为GBK等各种编码格式。
pass语句什么也不做,一般作为占位符或者创建占位程序,pass语句不会执行任何操作。
python提供了将变量或值从一种类型转换成另一种类型的内置函数,比如int函数能够将符合数字型字符串转换成整数。否则,返回错误信息。
copy仅拷贝对象本身,而不拷贝对象中引用的其他对象。 deepcopy除拷贝对象本身,而且拷贝对象中引用的其他对象。
序列化:将对象转换为字节序列的过程称为对象的序列化 反序列化:把字节序列恢复为对象的过程称为对象的反序列化
对象的序列化主要有两种用途: (1)把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。 (2)在网络上传送对象的字节序列。
在很多应用中,需要对某些对象进行序列化,让他们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些session先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为java对象。