C++基础知识总结
不管是自我定位太高,还是职位层次太低,系统复习了一遍很久没有摸过的C++总是有好处的。总结如下:
一、new和malloc的区别
1、new和delete配对,释放数组需要用delete[]。new和delete实际上调用了malloc和free,另外调用了类的构造函数和析构函数。
2、malloc和free配对,malloc返回的是void指针,需要强转。
3、new申请的内存保存在堆中,malloc申请的内存保存在自由存储区。
二、C++运算符
1、取模操作符:%
2、逻辑否、与、或:!, &&, ||
3、三元操作符:
c = (a>b) ? a : b;
4、按位与、或、非
& AND 逻辑与 Logic AND
| OR 逻辑或Logic OR
~ NOT 对1取补(位反转)Complement to one (bit inversion)
5、按位移:
<< SHL 左移Shift Left
>> SHR 右移Shift Right
三、&: 取地址运算符、定义变量引用
&操作符用于取地址时的用法是:int* x=&y;。
然而,另外一种用法是定义变量别名,这种用法不能和取地址简单等同。用于传递函数输入参数时很好理解,但定义变量时容易引起理解错误,特别是和指针的区别:
从内存的角度看,指针和引用是完全不同的。指针,内存要为它分配一个存储空间。引用,内存不分配空间的,引用只是一个别名。我认为就是在符号表里增加一个标志而已,对于语句int &y=x; (&x=&y)为true。
实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?答案是“用适当的工具做恰如其分的工作”。当重载某个操作符时,你应该使用引用。最普通的例子是操作符[]。这个操作符典型的用法是返回一个目标对象,其能被赋值。如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10;
但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。
引用的一些规则如下:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化),否则会报编译错误。
(2)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
以下示例程序中,k被初始化为i的引用。语句k = j并不能将k 修改成为j 的引用,只是把k的值改变成为6。由于k是i的引用,所以i 的值也变成了6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k 和i 的值都变成了6;
(3)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
以下的写法将地址指向一个位置的内存,是错误的。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生)
char *pc = 0; // 设置指针为空值
char& rc = *pc; // 让引用指向空值
(4)“sizeof 引用”得到的是所指向的变量(对象)的大小,但是当引用作为成员时,其占用空间与指针相同(没找到标准的规定)。
(5)引用只能指向一个实际的变量,不能指向指针或引用
(int*) * p1; // p1是指针的指针
(int*) & p2; // p2是指向整型指针的引用
引用不能指向指针或引用!
(int&) * p3; // ERROR: 不能有指向引用的指针,因为引用只是一个别名
(int&) & p4; // ERROR: 不能有指向引用的引用,因为引用只是一个别名
(6)指针和引用在内部的实现其实是没多大的区别的。但使用时有些地方是要注意的。因为引用具有对象行为,这一点很重要。引用复制时会调用对象的复制函数,在涉及多态时,这地方很容易出错。
class A{...};
class B:public A{...};
void f(A&a1,A&a2)
{
a1=a2;//此处调用的只有基类A的复制函数,而B部分不会被进行复制,之将导致数据的不一致(即B部分的数据没有被复制);
a1.fun();
}
四、关于const
一般的const变量:
下面两个声明都指向一个const int类型的指针,指针所指向的内存不能被修改,但指针可以指向另一个内存:
const int *p;
int const *q;
int类型的const指针应该这样声明。指针所指向的内存可以被修改,但指针不能指向另一个内存
int * const r= &n;
声明一个指向const int类型的const指针:
const int * const p=&n;
const在函数声明中的含义:
const int& SetPoint(const int& param) const
第一个const:
函数的返回值限定为const,即返回值不能被修改。const int a=SetPoint(...) a在此之后便不能被修改。
第二个const:
指函数的形参为const类型,函数体内不能被修改.
第三个const:
表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
类的const和static成员变量的初始化:
对于static成员变量,如果同时是const的,可以在类定义中初始化,否则只能在类定义外部初始化。
非static的const成员变量只能在构造函数的初始化列表中初始化。(ClassName():m_1(1){};)
五、一些数据类型和变量赋值语法
1、union 中的所有被声明的元素占据同一段内存空间,其大小取声明中最长的元素的大小。union 的用途之一是将一种较长的基本类型与由其它比较小的数据类型组成的结构(structure)或数组(array)联合使用。
2、long double和float变量的赋值方法:
3.14159L // long double
6.02e23f // float
3、容易引起理解错误的定义语句:int* p,q;
第一眼看去,好像是p和q都是int*类型的,但事实上,只有p是一个指针,而q是一个最简单的int型变量。同时定义两个指针的语法是:int *p1, *p2;
4、定义一个指向int[4]数组的指针变量
int (*p)[4]=RollNum;
这里,p被声明为一个指向一个4元素(int类型)数组的指针。
5、未指定size情况下,char数组的大小由初始化字符串决定:
我们可以用下面两种方法的任何一种来初始化字符串mystring:
char mystring [ ] = { 'H', 'e', 'l', 'l', 'o', '/0' };
char mystring [ ] = "Hello";
在两种情况下字符串或数组mystring都被定义为6个字符长(元素类型为字符char):组成Hello的5个字符加上最后的空字符('/0')。在第二种用双引号的情况下,空字符('/0')是被自动加上的。两种情况下sizeof应该都是6,strlen都是5。
六、常用的几个标准C++函数
1、cout和cin的用法:
cout << "xxx" << endl;
cin >> "yyy";
2、常用的字符串函数:
strcat //字符串拼接
strcpy
strncpy
strcmp //字符串比较,相同返回0
七、switch-case的写法
switch (expression)
{
case constant1:
block of instructions 1
break;
case constant2:
block of instructions 2
break;
.
.
.
default:
default block of instructions
}
八、函数的几个属性和用法
1、指定函数的默认参数值
int divide (int a, int b=2) {
2、什么是函数重载(Overloaded functions)
两个不同的函数可以用同样的名字,只要它们的参量(arguments)的原型(prototype)不同,也就是说你可以把同一个名字给多个函数,如果它们用不同数量的参数,或不同类型的参数。
3、内联函数
inline 指令可以被放在函数声明之前,要求该函数必须在被调用的地方以代码形式被编译。这相当于一个宏定义(macro)。它的好处只对短小的函数有效,这种情况下因为避免了调用函数的一些常规操作的时间(overhead),如参数堆栈操作的时间,所以编译结果的运行代码会更快一些。
调用函数的时候并不需要写关键字inline ,只有在函数声明前需要写。
4、将数组作为参数传入函数,传的是引用而不是值。
void procedure (int myarray[ ][3][4])
九、函数指针的用法
使用函数指针的几种方法:
(1)简单调用函数指针;
void (*pfunc)(int);
pfunc=callback_funcname;
callback_funcname(1);
其中声明函数指针原型的代码可以在调用处写,也可以写成全局的。这种方法使用简单,适用于临时调用。
(2)使用typedef调用函数指针:
typedef void(*PFUNC)(int);
PFUNC pfunc;
pfunc=callback_funcname;
callback_funcname(1);
这种方法适用于多次调用,先全局定义PFUNC,再在每个调用的地方声明临时变量后调用。
(3)C++类中调用成员函数指针(不使用typedef):
void (*MyClass::pfunc)(int);
pfunc=&MyClass::callback_funcname;
(this->*callback_funcname)(1);
和方法1类似,注意语法的不同。
(4)C++类中调用成员函数指针(使用用typedef):
typedef void(*PFUNC)(int); //在类中typedef
PUNC pfunc;
pfunc=&MyClass::callback_funcname;
(this->*callback_funcname)(1);
和方法2类似,注意语法的不同。
十、typedef的不常用用法
typedef的一般用法是:
typedef int UINT32;
但用来定义一个数组类型或指针函数时,比较特殊:
typedef char CARRAY[32]; //定义了一个CARRAY的类型,代表char[32]
typedef void(*PFUN)(int); //定义了一个指向指针函数的变量类型,函数原型为void xxx(int yyy);
十一、类的private/protected/public属性
1、类的成员如果没有指定访问域,默认是private的。
2、标识符protected 与 private类似,它们的唯一区别在继承时才表现出来。当定义一个子类的时候,基类的protected 成员可以被子类的其它成员所使用,然而private 成员就不可以。
3、public/protected/private继承的区别:
(1)public继承:父类的public依然是public,protected依然是protected,private不可访问;
(2)protected继承:父类的public称为protected,protected称为private;
(3)private继承:父类的所有成员全部变成private。
十二、关于空类
编译器为一个空类提供哪些默认函数?
1、C++编译器会提供默认的构造函数,析构函数, 拷贝构造函数和拷贝赋值操作符(请参考著名的Effective C++的第三版的第5条)
当我们定义一个class而没有明确定义构造函数的时候,编译器会自动假设两个重载的构造函数 (默认构造函数"default constructor" 和复制构造函数"copy constructor")。拷贝构造函数是一个只有一个参数的构造函数(原型:ClassName(ClassName &cn){};),该参数是这个class的一个对象,这个函数的功能是将被传入的对象(object)的所有非静态(non-static)成员变量的值都复制给自身这个object。
必须注意:这两个默认构造函数(empty construction 和 copy constructor )只有在没有其它构造函数被明确定义的情况下才存在。
一个类包含一个对赋值操作符assignation operator (=)的默认定义,该操作符用于两个同类对象之间。这个操作符将其参数对象(符号右边的对象) 的所有非静态 (non-static) 数据成员复制给其左边的对象。
2、用class obj;的方式声明一个对象,如果构造函数没有参数,或只有默认构造函数,后面不能加(),因为编译器会误以为这是一个没有参数的函数声明;
3、如果任何其它有任意参数的构造函数被定义了,默认构造函数和拷贝构造函数就都不存在了。在这种情况下,如果你想要有empty construction和copy constructor ,就必需要自己定义它们。
4、对基本类型,在c++里面,为了模板template,规定他们可以使用类似于类的默认构造函数的方式(仅仅是类似的方式而已) 赋初始值0。这叫做基本类型的显示初始化, 请参考 C++标准程序库(The C++ Standard Library)的14页,2.2.2 基本型别的显示初始化,书中举的例子就是
int i1;//未初始化
int i2 = int(); //初始化为0
sizeof一个空类等于多少?
sizeof一个空类返回1。所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。C++编译器不允许对象为零长度。试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。如果有自定义的变量,变量将取代这个缺省成员。
十三、继承或多重继承情况下构造函数的调用顺序
(1)如果声明为Derive: public Super1, public Super2{AnotherClass m_obj;}; 构造函数的调用顺序是:Super1, Super2, AnotherClass, Derive.
(2)如果父类有默认构造函数,或没有参数的构造函数,不需要在子类的构造函数定义中显式调用父类构造函数,否则需要调用。成员对象也是一样道理。以上面的例子说明,Derive的构造函数写法是:
Derive(int i): Super1(i), Super2(i), m_obj(1){...}
注意,成员对象初始化时应该指明对象名称,而不是类名。
析构函数的调用顺序应该是依次反过来的。
十四、虚函数、纯虚函数和抽象类、虚析构函数
虚函数的作用和运行原理
(1)多态是面向对象编程中的核心概念,就是说一个基类类型的指针实际上可能指向的是一个子类对象。只有在运行时才能根据实际情况来决定执行哪个函数,也就是动态联编。和动态联编对应的是静态联编,也就是说在编译时就决定了调用哪个函数。为了实现动态联编,必须将父类的函数声明为virtual。如果没有声明为virtual,可能得到的结果不是预期中的。
对于析构函数而言,虚函数保证子类和父类的析构函数都会被执行。
(2)对于包含了至少一个虚函数的类(或其父类包含虚函数),编译器需要为这个类增加4个字节,用来保存指向虚函数表VTABLE的指针。
纯虚函数和抽象类
包含了纯虚函数的类不能被直接实例化,可以称为抽象类。定义方法:
virtual void func()=0;
子类override一个虚函数,不一定要加virtual关键字。
什么情况下需要指定析构函数为virtual?
(1)析构函数不一定需要定义为虚函数,只有当这个类要作为其他类的父类使用时,才需要定义为虚函数。如果父类的析构函数没有定义为虚函数,则子类对象销毁时,父类析构函数不会被调用。
(2)对于一个抽象类,析构函数可以被定义为纯虚的。
(3)父类和子类之间的虚函数动态联编不会因为private发生影响。
(4)一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。
(5)在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。
十五、多重继承情况下如何引用父类的同名成员?
重继承情况下,如果多个基类有同名成员,引用方法是:
pDeriveObj->BaseClass1::Member;
十六、运算符重载
语法:
Type Type::operator +(const Type &i){}
十七、友元
友元可以实现外部对private和protected成员的访问。有两种实现:
(1)友元函数。语法:在函数声明前加上friend。友元函数并不是类的成员函数,实现函数体或调用函数时不加ClassName::。
(2)友元类。在类的声明中加入:friend class VisitorClass;
友元不会被子类继承。
十八、模板
函数模板
实现语法:
在函数的声明和实现前加上template<typename T> ,(typename和class等价),可以写在一行里面,也可以分成两行写,注意>后面没有分号。如果声明和实现分开写,两个地方都要写上template<typename T>。
(1)一行代码中同时声明并实现函数
template <typename T>void func(T t1, T t2)
{...};
(2)分两行代码声明并实现函数
template <class T>
void func(T t1, T t2)
{...};
(3)在两个地方分别声明和实现函数
template <class T>
void func(T t1, T t2);
...
template <class T>
void func(T t1, T t2)
{...}
引用语法:
func<int>(5, 6);
类模板
声明方法:
类模板可以实现在一个类中有一个通用类型的成员变量。
template <class T> class ClassName
{public:
T *m_pVariable;
};
引用方法:
ClassName<int> obj;
模板特殊化
模板特殊化可以专门为某种数据类型定义特殊的行为。类的定义必须和通用的模板类完全一致,除了用专门语法,并将T修改为专门的类型,并定义特殊行为。
template<> class<int>{定义通用函数,定义特殊函数};
定义模板的默认值
template <class T = char> // 有一个默认值。
模板的参数值
除了模板参数前面跟关键字class 或 typename 表示一个通用类型外,函数模板和类模板还可以包含其它不是代表一个类型的参数,例如代表一个常数,这些通常是基本数据类型的。
二十、类型转换和C++高级类型转换
基本类型强转有两种写法:
int i;
float f = 3.14;
i = (int) f;
i = int ( f );
高级类型转换
ANSI-C++ 标准定义了4种新的类型转换操作符: reinterpret_cast, static_cast, dynamic_cast 和const_cast。
reinterpret_cast可以将一个指针转换为任意其它类型的指针。
ClassA* pa;
ClassB* pb=reinterpret_cast<ClassB*>pa;
static_cast可以执行所有能够隐含执行的类型转换,以及它们的反向操作(即使这种方向操作是不允许隐含执行的)。用于类的指针,也就是说,它允许将一个引申类的指针转换为其基类类型(这是可以被隐含执行的有效转换),同时也允许进行相反的转换:将一个基类转换为一个引申类类型。不会检查被转换的基类是否真正完全是目标类型的。
Derive* pa;
Super* pb;
pa = static_cast<Derive*> pb;
pb = static_cast<Super*> pa;
static_cast除了能够对类指针进行操作,还可以被用来进行类中明确定义的转换,以及对基本类型的标准转换:
double d=3.14159265;
int i = static_cast<int>(d);
dynamic_cast 完全被用来进行指针的操作。它可以用来进行任何可以隐含进行的转换操作以及它们被用于多态类情况下的方向操作。然而与static_cast不同的是, dynamic_cast 会检查后一种情况的操作是否合法,也就是说它会检查类型转换操作是否会返回一个被要求类型的有效的完整的对象。
在不合法的情况下,如果用于指针,将返回NULL;如果用于引用,抛出异常。
Derive* pa = new Derive();
Super* pb = new Super();
pa = dynamic_cast<Derive*> pb; //失败,返回NULL
pb = dynamic_cast<Super*> pa; //成功
const_cast类型转换对常量const 进行设置或取消操作。
class C {};
const C * a = new C;
C * b = const_cast<C*> (a);
typeid (object_pointer)
这个操作符返回一个类型为type_info的常量对象指针,这种类型定义在标准头函数中。type_info::name()返回对象的类名。
二十一、命名空间
定义一个命名空间:
namespace ns1{...}
设置默认命名空间:
using namespace ns1;
引用其他命名空间的类型:
ns2::variable = xx;
二十二、预处理命令
#undef 完成与 #define相反的工作,它取消对传入的参数的宏定义
#ifdef, #ifndef, #if, #endif, #else and #elif
指令#line 可以使我们对这两点进行控制,也就是说当出错时显示文件中的行数以及我们希望显示的文件名。它的格式是:
#line number "filename"
下面这段代码将会产生一个错误,显示为在文件"assigning variable", line 1 。
#line 1 "assigning variable"
int a?;
这个指令将中断编译过程并返回一个参数中定义的出错信息
#error
这个指令是用来对编译器进行配置的,针对你所使用的平台和编译器而有所不同。
#pragma
二十三、预定义宏
__LINE__ 整数值,表示当前正在编译的行在源文件中的行数。
__FILE__ 字符串,表示被编译的源文件的文件名。
__DATE__ 一个格式为 "Mmm dd yyyy" 的字符串,存储编译开始的日期。
__TIME__ 一个格式为 "hh:mm:ss" 的字符串,存储编译开始的时间。
__cplusplus 整数值,所有C++编译器都定义了这个常量为某个值。如果这个编译器是完全遵守C++标准的,它的值应该等于或大于199711L,具体值取决于它遵守的是哪个版本的标准。
以上是 C++基础知识总结 的全部内容, 来源链接: utcz.com/z/346658.html