概述
封装
作用域解析运算符
:: 作用域解析运算符
::a++; //全局变量a
public
struct的成员默认为public
protected
private
class成员默认为private
friend 友元
允许非类的成员函数访问类的成员变量
一个friend必须在一个类内声明
也可以把整个类声明为friend
嵌套友元
类中嵌套的结构或类,并不能访问类的private成员,如果想访问的话,必须做如下操作
1.声明嵌套的类或结构
2.声明嵌套的类或结构是友元
3.定义嵌套的类或结构
类型转换
C++是的类型转换非常严格,比如其他类型指针可以赋给void*,但void*不能赋给任何类型指针,必须显式转换
显式转换
static_cast
用于”良性“和”适度良性“转换,包括不用强制转换(例如自动类型转换)
C++不允许从void*赋值,除非用转换符
void *p; int *pi;
pi = p; //错误
i = static_cast<int *>(p);
const_cast
对const和/或volatile进行转换
从const转为非const,从volatile转换为非volatile
reinterpret_cast
转换为完全不同的意思。为了安全使用它,关键必须转换回原来的类型。转换成的类型一般只能用于位操作,否则就是为了其他隐秘的目的。这是所有类型转换中最危险的
dynamic_case
用于类型安全的向下转换
仅当类型转换时正确的并且成功时,返回一个所需类型的指针,否则它将返回0。
无论何时进行向下类型转换,都要判断是否为0
dynamic_case有开销,如果有大量的dynamic_case,会影响性能
自动类型转换
构造函数转换
如果定义一个构造函数,能以另一类型的对象做为单个参数,那么这个构造函数允许编译器执行自动类型转换
创建一个单一类型参数的构造函数总是会定义一个自动类型转换函数
阻止构造函数转换 前面加explicit
运算符转换
创建一个成员函数,通过在operator后跟随想要转换的类型的方法,将当前类型转换为希望的类型
没有返回类型,返回类型就是正在重载的运算符的名字
operator A() { return a; } 将当前类型,转换为A类型
当提供了不止一种的类型转换的时候,会发生冲突。自动类型转换应小心使用
自动类型转换只发生在函数调用中,不在成员选择期间
初始化与清除
构造函数
构造函数与类同名,没有返回值,可以有参数
只要有构造函数,所有初始化工作必须通过构造函数完成,编译器保证不管什么情况都会调用构造函数
默认构造函数——不带任何参数的构造函数
聚合初始化
如果初始值多余元素个数,则出错;如果少于元素个数,其余元素赋0
int a[5] = {1,2,3,4,5}; int b[5] = {4}; int c[] = {1,2,3,4};
拷贝构造函数
如果没有拷贝构造函数,编译器会生成一个默认拷贝构造函数,执行位拷贝
如果自定义拷贝构造函数,编译器执行用户定义的拷贝构造函数
对于组合对象,编译器会递归地为所有成员对象和基类调用拷贝构造函数
最好创建自己的拷贝构造函数而不是使用编译器默认创建的
如果想防止按值传递,可以定义一个私有的拷贝构造函数
赋值运算符重载
Object a = b; 调用拷贝构造函数
a = c; 调用operator=,因为a已经被创建,不能再调用拷贝构造函数了
如果对象还没有被创建,初始化是需要的;否则使用operator=
最好避免用operator=初始化代码,而是显式地调用构造函数
所有赋值运算符重载,必须检测自赋值 if( this == &right ) return *this;
如果没有显式创建operator=,编译器会自动创建,规则与拷贝构造函数相同
析构
析构函数与类同名,前面加~,没有返回值,没有参数
非局域的goto语句将不会引发析构函数的调用,有goto语句时,需要留意 ???
显式地调用析构函数
a.A::~A();
只有在定位new时才使用,其他case不要使用
重载
函数重载
编译器会根据函数名和参数列表生成内部函数名
不通过返回值生成内部函数名,是因为有时可以忽略返回值
所有函数在调用前必须被声明,编译器不会猜测函数的声明
运算符重载
运算符重载
运算符重载只是语法上的方便,是另一种函数调用的方式
只有包含用户定义类型的运算符才能重载,只包含内建类型的运算符不能重载
不能重载没有意义的运算符,不能改变运算符优先级,不能改变运算符参数
语法
成员函数
一元运算符重载 没有参数
二元运算符重载 一个参数
全局函数
一元运算符重载 一个参数
二元运算符重载 两个参数
可重载运算符
一元运算符
+ - ~ & !
a++ operator++(a, int)
++a operator++(a)
a-- operator--(a, int)
--a operator--(a)
二元运算符
+ - * / %
^ & | << >>
+= -= *= /= %= ^= &= |= >>= <<=
== != < > <= >=
&& ||
=
不常用的运算符
[] 必须是成员函数,只接受一个参数
-> 必须是成员函数;必须返回一个对象,该对象也有一个指针间接运算符,或者必须返回一个指针,被用于选择指针间接引用运算符箭头所指向的内容
->* 同->,只不过必须返回一个对象
() 必须是成员函数,它是唯一允许带不定参数的函数
new delete ,
不可重载运算符
. .*
重载方针
所有一元运算符 建议重载成成员函数
= () [] -> ->* 必须是成员函数
+= -= *= /= ^= &= |= %= >>= <<= 建议重载成成员函数
所有其他二元运算符 建议重载成非成员函数
常量
值替代
应该完全用const取代#define的值替代
const默认是内部链接,外部文件无法引用
定义const时用extern修饰,会把const值变为外部链接;其他文件引用时,用extern声明
只有声明了extern,或对const常量取址时,编译器才为const常量分配存储空间,否则不分配
定义一个const时,必须赋一个初值,这样才能区分声明和定义
const数组,表示不能改变的一块存储空间
不能在编译时使用const数组的值,但是在运行时可以
C语言的const,表示一个不能被改变的普通变量
C++的const,表示一个常量,并且往往没有分配存储空间
指针
指向const的指针
const int *p; p是一个指针,指向const int,p指向的值不能被改变
int const *p; 同上。可读性不如上面
const指针
int i;
int* const p = &i; p是一个const指针,指向一个int。因为p是const指针,所以必须赋初值。p不能再指向别的int,但指向的int的内容可以改变
const int* const p = &i; p是const指针,指向一个const int
int const* const p = &i; 同上
类型检查
可以把一个非const对象的地址赋给一个const指针,但不能把一个const对象的地址赋给一个非const指针
参数和返回值
传递const值
如果按值传递,const限定无意义;如果按地址传递,const可以保证地址内容不会被改变
返回const值
如果按值返回const内建类型,const限定无意义,因为编译器已经不让它成为一个左值
如果按值返回const用户定义的类型,那么这个返回值不能是一个左值,即不能被再次赋值
临时量
X f1();
void f2(X &x);
f2(f1()); //错误
1.编译器必须产生一个临时对象保存f1()的返回值
2.如果f2()的参数是按值传递的,则没有问题;
3.如果f2()的参数是按引用传递的,意味着f2()必须取临时对象的地址,而f2()的参数没有用const修饰,可能会对临时对象进行修改,所以会报错
标准参数传递
当传递一个参数时,首先选择按引用传递,而且是const引用
当临时变量按引用传递给一个函数时,函数的参数必须是const引用
类
类里的const
在每个类对象里分配存储空间并代表一个值,这个值一旦被初始化后就不能被改变
在这个对象的生命期内,它是一个常量,但对于这个常量来说,在每个不同对象的值可能会不同
类里普通的const不能赋初值,初始化工作必须在构造函数初始化列表中进行
构造函数初始化列表:在构造函数执行前,初始化列表已经将所有const初始化完毕
A::A( int a, int b = 0, int c = 1 ):size(1){}
编译期间类里的常量
内建类型的static const可以看做一个编译期间的常量
必须在static const定义的地方对它进行初始化
enum hack
早期C++不支持static const,用匿名enum代替,enum不分配存储空间 enum{ size = 100, };
const对象和成员函数
const对象
在对象的生命周期内,数据成员的值不会被更改
const成员函数
const成员函数,告诉编译器该函数不会改变对象的任何数据成员,也不会调用任何非const的成员函数,所以可以被一个const对象所调用
一个没有被明确声明为const的成员函数,被看成是将要修改数据成员的函数,不能被const对象所调用
必须在声明和定义函数时,在参数列表后面重申const属性,否则编译器会认为他们是不同的函数
不修改数据成员的任何函数,都应该被声明为const,这样就可以被const对象调用
const成员函数如何改变类的数据成员
按位const
对象的每个比特位都不允许改变
按逻辑const
对象从整体概念上是不变的,但是可以以成员为单位改变
强制转换常量性 —— 缺少明确的声明,不建议这样做
将congst对象的this指针转换为非const,然后再改变数据成员
((A*)this)->i++;
(const_cast<A*>(this))->i++;
mutable
将需要改变的数据成员,用mutable声明
volatile
表示变量或对象可能被外部改变,编译器在优化代码时需要注意,每次访问时必须重新读取
volatile与const语法一样,可以修饰数据成员、成员函数和对象本身
编译器不优化时,volatile可能不会起作用;一旦开始优化代码时,volatile声明可以防止出现重大错误
c-v限定词
表示const或volatile中的任何一个,c-v qualifier
静态元素
C语言中的静态元素
函数内部静态变量
无论什么时候设计包含静态变量的函数时,都要记住多线程问题
内建类型的静态变量,编译器自动初始化该变量为0
静态对象
静态对象必须用构造函数初始化,如果定义静态对象时没有指定构造函数参数,那么该类必须有默认构造函数
静态对象的析构
如果调用abort()退出程序的话,静态对象的析构函数不会被调用
静态对象的析构函数,在main()函数退出,或调用exit()被调用,所以在静态对象析构函数中调用exit()非常危险,这样会导致无穷递归调用
atexit()指定跳出main()或调用exit()的操作,atexit()的注册函数中可以在所有对象析构前被调用
全局对象在main()前被调用,在退出main()时被销毁
如果一个静态对象从未被调用过,那么该对象不会被创建,退出程序时该对象也不会被析构
C++的静态成员
内建类型的静态变量
static int i; 在外部初始化
内建类型的静态常量
static const int i = 0; 当声明的地方初始化
对象和数组静态变量 在外部初始化
static X x;
static int a[];
对象和数组静态常量 在外部初始化
static const X x;
static int a[];
不能在局部的类中定义静态成员
静态成员函数
不能访问一般的数据成员,只能访问静态数据成员
只能调用静态成员函数
静态成员函数没有this
静态/全局变量初始化的相依性
如果/全局变量之间的初始化是相互依赖的,在移植时可能会碰到问题
解决方案
1.避免初始化时的互相依赖
2.把关键的/全局对象的定义放在一个文件中
3.在头文件中增加一个额外的类,这个类负责静态对象的动态初始化
4.把一个静态/全局对象放在能返回对象引用的函数中,这样能保证调用时会被初始化,而且只初始化一次
内联函数
C++中宏的限制
只是单纯的替换,隐藏难以发现的错误
预处理器不允许访问类的数据成员
参数展开的副作用 例如i++ ++i
内联函数
内联函数能够像普通函数一样的行为,唯一不同之处是,内联函数会在适当的地方像宏一样展开
任何在类中定义的函数自动成为内联函数。应该只使用内联函数而不使用宏
非类的函数前面加上inline关键字也可成为内联函数,但函数体必须和声明结合在一起,否则inline声明是无效的
使用内联函数的目的是减少开销,如果滥用内联函数的话,会造成代码膨胀,因为内联函数会像宏一样展开
编译器对内联函数的实现
编译器会把内联函数的代码放入符号表,然后在合适的时机插入到程序中
只有在类声明结束后,其中的内联函数才会被计算,所以内联函数中可以向前引用
友元的内联
在类中定义一个友元函数为内联函数,不会改变友元的状态,它仍是全局函数而不是类的成员函数
内联函数的限制
函数太复杂
有任何形式的循环
语句太多,引起代码膨胀
产生递归调用
显示或隐式的寻址
当地址不需要时,编译器仍可能内联代码
使用宏的情况
# #define PRINT(x) cout << #x << " = " << x << endl;
## 标志粘贴
名字空间
创建
namespace只能在全局范围定义,但可以嵌套
namespace的结尾不需要有分号
namespace可以在多个文件中延续定义
可以用一个namespace作为另一个namespace的别名
不能创建namespace的实例
可以在一个namespace的类定义中插入一个友元
使用
作用域运算符
NameSpaceX::A::f();
using namespace std; using指令
把一个namespace的所有名字引入到另一个空间内
如果多个namespace定义了相同的名字,在使用这些名字时,会引起冲突
using std:string; using声明
把一个名字引入到当前的范围
会覆盖其他using namespace中同名的名字
因为没有类型信息,所以如果using的名字有重载,则会引入该名字所有的函数
一般在cpp中使用using指令或声明,这样不会影响到其他cpp文件;不要在头文件中使用using namespace,因为这样包含该头文件的所有cpp文件都会受到影响
引用
创建引用时,必须被初始化
一旦一个引用被初始化为指向一个对象,就不能改变为另一个对象的引用,而指针可以
不可能有NULL引用
一旦从函数返回一个引用,必须像从函数返回一个指针一样对待,当函数返回时必须保证引用的内存还在
动态对象创建
C语言分配内存调用库函数,C++将动态对象创建作为语言的核心,使用运算符
malloc分配的内存用delete释放,行为未定义
对一个对象delete两次可能会有问题;delete空指针不会有问题
delete void*可能会出错,因为它只delete,而不执行析构函数
delete数组:delete [] fp;
new失败时,不会执行构造函数,一个new-handler的函数会被调用,可以用set_new_handler注册回调函数
重载new和delete
全局重载
重载类的new和delete
会覆盖全局的重载版本
尽管不必显式地使用static,但实际上仍是在创建static成员函数
重载类的new和delete的数组形式
继承和组合
继承
private
继承时,基类中所有的成员都默认是private的
如果希望基类中的成员可视,在派生类的public部分声明名字即可,会让基类中改名字的所有版本变为public
public
基类中的所有公共成员在派生类中依旧是公共的
protected
只是为了语言的完备性
外部用户无法访问基类的protected成员,只有派生类中可以访问
可以允许多重继承
构造函数初始化列表的意义
在进入新类的构造函数之前,保证基类和其他对象成员都已经构造好了
构造和析构的顺序
构造是从类层次的最根处开始,在每一层,先调用基类的构造函数,再调用成员对象的构造函数
成员对象构造函数的调用次序,不受构造函数初始化列表的顺序影响,而由成员对象在类中声明的顺序决定
析构的顺序严格按照构造的相反顺序
重载基类成员函数
任何时候重定义基类中的一个成员函数,无论是重载还是更改类型,都会将基类中该函数所有版本隐藏起来
非自动继承的函数
构造函数
拷贝构造函数
编译器会首先自动调用基类的拷贝构造函数,然后再是个成员对象的拷贝构造函数
无论何时我们在创建了自己的拷贝构造函数时,都要正确的调用基类拷贝构造函数
operator=
析构函数
向上类型转换
新类属于原有类的类型。从派生类到基类的类型转换,叫做向上类型转换
指针和引用也可以向上类型转换
确定有组合还是继承,最清楚的方法之一,是询问是否需要从新类向上类型转换
多态和虚函数
捆绑
捆绑 binding
把函数体与函数调用相联系
早捆绑 early binding
编译时捆绑
晚捆绑 late binding( 动态捆绑 dynamic binding 运行时捆绑 runtime binding )
运行时捆绑
晚捆绑的实现
编译器对每个包含虚函数的类或从包含虚函数的类派生一个类时,创建一个唯一的表VTABLE
编译器在每个带有虚函数的类中放置一个指针VPTR,指向这个对象的VTABLE
不管有几个虚函数,都只有一个VPTR
如果编译器知道对象确切的类型,那么不实现晚捆绑
虚函数
定义
对于特定的函数,为了引起晚捆绑,C++要求在基类中声明这个函数时使用virtual关键字
晚捆绑只对virtual函数起作用,而且只在使用含有virtual函数的基类的地址时发生
仅仅在声明时使用关键字virtual,定义时不需要
如果一个函数在基类中被声明为virtual,那么在所有的派生类中都是virtual的
编译器会自动地调用继承层次中最近的virtual定义,保证总可以调到某个虚函数
虚函数不是相当高效的,C++为了效率,将虚函数设计成可选的,像java、Python这样的语言,虚函数是默认的
抽象基类与纯虚函数
如果仅想对基类进行向上类型转换,使用它的接口,不希望用户实际地创建一个基类对象,可以在基类中加入至少一个纯虚函数,使基类成为抽象类
当继承一个抽象类时,必须实现所有纯虚函数,否则继承出的类也是一个抽象类
如果试图建立一个抽象类的对象,编译器会产生错误
virtual void function() = 0;
可以在基类中对纯虚函数提供定义,但不能定义成内联函数
对象切片
当多态地处理对象时,都是传地址而非传值
如果对一个对象的值进行向上类型转换,这个对象会被切片(copy到一个新对象时,去掉原来对象的一部分)
虚函数的重定义
不能更改虚函数的返回值
如果重新定义一个基类的虚函数,其所有重载版本全部隐藏
如果返回一个指向基类指针的引用,则可以重定义为返回派生类的指针或引用
虚函数和构造函数
构造函数不能为虚函数
虚机制在构造函数中不工作,调用本地的版本
虚函数和析构函数
析构函数能够且常常必须是虚的。不把析构函数设为虚函数是一个隐匿的错误
虚机制在析构函数中不工作,调用本地的版本
纯虚析构函数
必须在基类中为纯虚析构函数提供一个定义,因为基类在析构时必须要有函数体
不要求在派生类中重新定义纯虚析构函数,因为编译器会自动生成
纯虚析构函数的唯一效果是使类成为一个抽象类
模板
模板实现了参数化类型的概念
通常把模板的所有声明和定义都放入一个头文件中
在template<...>之后的任何东西都意味着编译器在当时不为它分配存储空间,而是一直处于等待状态直到被一个模板示例告知
首先创建和调试一个普通类,然后让它成为模板,一般认为这种方法比一开始就创建模板更容易
函数模板——通用算法
其他
头文件
#include <> 在默认和指定的路径寻找头文件
#include <iostream> C++标准形式,没有.h
#include <stdio.h> 包含C头文件,有.h
#include <cstdio> 包含C头文件,前面加"c"
#include "myproject.h" 现在当前路径下寻找,然后再按照<>方式寻找
必须加.h
register
告诉编译器,尽可能快地访问这个变量,要求编译器将此变量放入寄存器中
不能保证编译器会将变量放入寄存器中,甚至也不能保证能提高访问速度,这只是对编译器的一个暗示
不能定义全局或静态register变量,只能定义局部register变量
不能得到或计算register变量的地址
可以使用register变量定义函数形参
最好避免使用register变量
asm
允许在C++中写汇编代码
第一个字符是下划线的标识符是保留的,不应该使用它们
返回值优化
return Object(); 直接返回一个对象。如果创建一个对象再返回,需要调用构造函数,拷贝构造函数和析构函数;直接返回对象则直接把对象创建在外面,只调用构造函数
默认参数
在声明时给定一个值,如果在调用时没有指定这一参数的值,编译器会自动插上这个值
int f( int a = 0 )
默认参数只能放在声明中
只有参数列表的后部参数才是可默认的,不可以在一个默认参数后面,又跟一个非默认的参数
一旦在一个函数调用中开始使用默认参数,那么这个参数后面的所有参数都必须是默认的
占位符参数
void func( int a, int, int c )
a和c可以被引用,中间的参数不行
调用时,也需要为占位符提供一个值
可以防止告警
选择重载还是默认参数
基本原则是,不能把默认参数作为一个标志去决定执行函数的哪一块,如果出现这种情况,应该把函数分解成两个或多个重载的函数
默认参数应该是一个在一般情况下放在这个位置的值,这个值出现的可能性比其他值要大,所以客户程序员可以忽略它,或只在需要改变默认值的情况下才去用它
默认参数的意义是使函数调用更容易
默认参数的另一个重要应用是,开始定义函数时定义了一组参数,后来发现要增加一些参数。通过把新增的参数设置为默认参数,可以保证以前的代码不受影响
替代链接说明
在C++中引用C函数,C++编译器会把函数名加上参数类型以支持重载,导致链接器找不到函数
extern "C" {}
成员指针
class Object{ int a,b,c; }
Object obj, pobj = &obj;
int Object::*p = &Object::a;
obj.*p = 1;
pobj->*p = 2;
p = &Object::b;
int (Object::*func)(int a) const = &Object::func;
最后
以上就是大胆砖头为你收集整理的C++学习(一)——基本语法的全部内容,希望文章能够帮你解决C++学习(一)——基本语法所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复