易混淆的c++知识点

初始化和赋值的区别

在定义一个变量或常量时为它指定初值叫做初始化,而在定义一个变量或常量以后使用戚值运算符修改它的值叫做赋值,勿将初始化与赋值混淆。

类的组合和友元函数

类组合的情况,类B 中内嵌了类A 的对象,但是B 的成员函数却元法直接访问A 的私有成员x 。
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。,通过友元关系,一个普通函数或者类的成员函数可以访问封装于另外一个类中的数据。友元函数是在类中用关键字friend 修饰的非成员函数。
若A 类为B 类的友元类,则A 类的所有成员函数都是B 类的友元函数, 都可以访问B类的私有和保护成员。
组合类构造函数定义的一般形式为:
Circle::Circle(float r): radius® {}

常对象

如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数(这就是c++ 从语法机制上对常对象的保护,也是常对象唯一的对外接口方式)。
此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用const 修饰的成员函数(这就保证了在常成员函数中不会更改目的对象的数据成员的值)。
const 关键字可以用于对重载函数的区分。例如,如果在类中这样声明:
void print ();
void print () const;
习惯:在适当的地方使用const 关键字,是能够提高程序质量的一个好习惯。对于无须改变对象状态的成员函数,都应当使用const
常引用
如果在声明引用时用const 修饰,被声明的引用就是常引用。常引用所引用的对象不能被更新。如果用常引用作形参,便不会意外地发生对实的更改。

指针与引用

引用与指针的一个显著区别是,普通指针可以多次被赋
值,也就是说可以多次更改它所指向的对象,而引用只能在初始化时指定被引用的对象,其后就不能更改。因此,引用的功能,与一个指针常量差不多。
需要指出的是,虽然在"读取v 的地址"这一用途上p 和&r 是等价的,但p 和&r 却具有不同的含义.p 可以再被取地址,而&r 则不行。也就是说,引用本身(而非被引用的对象)的地址是不可以获得的,引用经定义后,对它的全部行为,全是针对被引用对象的,而引用本身所占用的空间则被完全隐藏起来了。因此,引用的功能还是要比指针常量略差一点。
只有常引用,而没有引用常量,也就是说,不能用T &. const 作为引用类型。
这是因为引用只能在初始化时指定它所引用的对象,其后则不能再更改,这使得引用本身(而非被引用的对象)已经具有常量性质了。
对于数据参数传递、减少大对象的参数传递开销这两个用途来说,引用可以很好地代替指针,使用引用比指针更加简洁、安全。其中,如果仅仅是为了后一个目的,一般来说应当使用常引用作为参数。

如果在声明引用时用const 修饰,被声明的引用就是常引用。常引用所引用的对象不能被更新。
const 类型说明符&引用名;
非const 的引用只能绑定到普通的对象,而不能绑定到常对象,但常引用可以绑定到常对象。一个常引用,无论是绑定到一个普通的对象,还是常对象,通过该引用访问该对象时,都只能把该对象当作常对象

(1)可以声明指向常量的指针,此时不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象

int a;
const int *p1= &a;
int b;
p1= &b;//正确, p1 本身的值可以改变
*pl= 1;//编译时出错,不能通过p1 改变所指的对象

(2) 可以声明指针类型的常量,这时指针本身的值不能被改变。例如:
int *const p2= &a;
p2= &b; //错误, p2 是指针常量,值不能改变

指针的运算

指针算术运算的用途,一定要限制在通过指向鼓组中某个元素的指针,得到指向同一个敢组中另一个元素的指针。指针算术运算的其他用法,都会得到不确定的结果。
整型的2 和双精度浮点型的2. 在内存中是由不同的二进制序列表示的。不过,在执
行static_cast (i) 这一操作时,编译器会生成目标代码,将i 的整型的二进制表示,转换成浮点型的二进制表示,这种转换叫做基于内容的转换。但是有指针参加的转换,情况就不大一样了,例如:
int i= 2;
float*p= reinterpret cast< float *> (&i);
reinterpret_cast 是和static_ cast 并列的一种类型转换操作符,它可以将一种类型的指针转换为另一种类型的指针,这里把int* 类型的&i 转换为float *类型。
reinterpret_cast 不仅可以在不同类型对象的指针之间转换,还可以在不同类型函数的指针之间、不同类数据成员的指针之间、不同类函数成员的指针之间、不同类型的引用之间相互转换。reinterpret_ cast 的转换过程,在c+ 十标准中未明确规定,会因编译环境而异。c++ 标准只保证用reinterpret_cast 操作符将A 类型的p 转换为B 类型q ,
再用reinterpret_cast 操作符将B 类型的q 转换为A 类型的r 后电应当有(p= =r) 成立。

二维数组:

int line1[]={l , 0, 0};
int line2[]={0, 1, 0};
int line3[]= {0, 0, 1};
int pLine[3]={line1 , line2 , line3};
pLine[i] [j] 与
(pLine[i]+j) 等价,即先把指针数组pLine 所存储的第i 个指针读出,然后读取它所指向的地址后方的第j 个数。它在表示形式上与访问二维数组的元素非常相似,但在具体的访问过程上却不大一样。
二维数组在内存中是以行优先的方式按照一维顺序关系存放的。因此对于二维数组,可以将其理解为一维数组的一维数组,数组名是它的首地址,这个数组的元素个数就是行数,每个元素是一个一维数组。

在程序运行时,不仅数据要占据内存空间,执行程序的代码也被调入内存并占据一定的空间。每一个函数都有函数名,实际上这个函数名就表示函数的代码在内存中的起始地址。由此看来,调用函数的通常形式"函数名(参数表)“的实质就是"函数代码首地址(参数表)”。

函数指针

声明一个函数指针时,也需要说明函数的返回值、形式参数列表,其一般语法如下:
数据类型(头函数指针名) (形参表)
数据类型说明函数指针所指函数的返回值类型;第一个阿括号中的内容指明一个的数指针的名称;形参表则列出了该指针所指函数的形参类型和个数。
typedef int (*DoubleInt Fu nction) (double);
这声明了DoublelntFunction 为"有一个double 形参、返回类型为int 的函数的指针"
类型的别名。下面,需要声明这一类型的变量时,可以直接使用:
DoubleIntFunction funcPtr;

void printStuff (float) {
    
    
cout<< "This is the print stuff function. "<< endl;

}
void (* functionPointer) (float) ;
printStuff(PI);
functionPointer=printStuff;//函数指针指向prìntStuff

作用域

作用域可见性的一般规则如下。
.标识符要声明在前,引用在后。
·在同一作用域中,不能声明同名的标识符。
·在没有互相包含关系的不同的作用域中声明的同名标识符,互不影响。
·如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见。
如果对象的生存期与程序的运行期相同,则称它具有静态生存期。在命名空间作用域中声明的对象都是具有静态生存期的。如果要在函数内部的局部作用域中声明具有静态生存期的对象,则要使用关键字statìc 。
局部作用域中静态变量的特点是,它并不会随着每次函数调用而产生一个副本,也不会随着函数返回而失效。也就是说,当一个函数返回后,下一次再调用时,该变量还会保持上一回的值,即使发生了递归调用,也不会为该变量建立新的副本,该变量会在每次调用间共享。
在定义静态变量的同时也可以为它赋初值,例如:
static int i= 5;
这表示i 会被赋予5 初始化,而非每次执行函数时都将i 赋值为5 。
定义时未指定初值的基本类型静态生存期交量,会被赋予0 值初始化,而对于动态生存期变量,不指定初值意味着初值不确定。
局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时。

外部变量

如果一个变量除了在定义它的源文件中可以使用外,还能被其他文件使用,那么就称
这个变量是外部变量。命名空间作用域中定义的变量,默认情况下都是外部变量,但在其他文件中如果需要使用这一变量,需要用extern 关键字加以声明。
。外部变量是可以为多个游、文件所共享的全局变量。

在包含多个源文件的工程中,匿名命名空间常常被用来屏蔽不希望暴露给其他源文件的标识符,这是因为每个源文件的匿名命名空间是彼此不同的,在一个源文件中没有办法访
问其他源文件的匿名命名空间。

  • 在同一作用域中,不能声明同名的标识符。

  • 在没有互相包含关系的不同的作用域中声明的同名标识符,互不影响。

  • 如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见。
    常数据成员只能通过初始化列表来获得初值
    class A (
    public:
    A(int i);
    void print () ;
    private:
    const int a;
    static const int b;}

//静态常数据成员
const A: :b= 10;
//静态常数据成员在类外说明和初始化
//常数据成员只能通过初始化列表来获得初值
A::A(int i) : a(i) {
    
    }
int main () (
//建立对象a 和b ,并以100 和0 作为初值,分别调用构造函//数,通过构造函数的初始化列表给对象的常数据成员赋初值/
A a1 (100) ,a2 (0);
a1. print ();
a2.print();
return 0;
}

细节类成员中的静态变量和常量都应当在类定义之外加以定义,但c++ 标准规定了一个例外:类的静态常量如采具有整数类型或枚举类型,那么可以直接在类定义中为它指定常量值,例如,可以直接在类定义中写:
static const int b=10;

undef 的作用是删除由# define 定义的宏,使之不再起作用。
一个新的关键字mutable 。对于某类数据成员,可以使用
mutable 关键字加以修饰,这样,即使在常成员函数中,也可以修改它们的值。被mutable 修饰的成员对象在任何时候都不会被视为常对象,这是mutable 更一般的含义。
程序是存储在磁盘上的,在执行前,操作系统需要首先将它载入到内存中,并为它分配足够大的内存空间来容纳代码段和数据段,然后把文件中存放的代码段和初始化的数据段的内容载入其中一部分静态生存期对象的初始化就是通过这种方式完成的,

内存空间的访问方式

在c++ 程序中如何利用内存单元存取数据呢?一是通过变量名,二是通过地址。程序中声明的变量要占据一定的内存空间,例如,对于一些常见的32 位系统, short 型占2 字节.long 型占4 字节。具有静态生存期的变量在程序开始运行之前就已经被分配了内存空间。具有动态生存期的变量,是在程序运行时遇到变量声明语句时被分配内存空间的。变量获得内存空间的同时,变量名也就成了相应内存空间的名称,在变量的整个生存期内都可以用这个名字访问该内存空间,表现在程序语句中就是通过变量名存取变量内容。但是,有时使用变量名不够方便或者根本没有变量名可用,**这时就需要直接用地址来访问内存单元。**例如,在不同的函数之间传送大量数据时,如果不传递变量的值,只传递变量的地址,就会减少系统开销,提高效率。如果是动态分配的内存单元(将在6.3 节介绍) .则根本就没有名称,这时只能通过地址访问。

point= new int (2) ;
动态分配了用于存放int 类型数据的内存空间,并将初值2 存入该空间中,然后将首地址
赋给指针point 。
如采保留括号,但括号中不写任何数值,则表示用。对该对象初始化,例如:
int *point= new int () ;

assert

assert 只在调试模式下生效,而在发行模式下不执行任何操作,这样兼顾了调试模式的调试需求和发行模式的效率需求。
提示由于assert 只在调试模式下生效,一般用assert 只是检查程序本身的逻辑错误,而用户的不当输入造成的错误,则应当用其他方式加以处理。

以指针形式访问数组元素:

int main() {
    
    
f1oat (*cp) [9] [8]=new f1oat[8] [9] [8];
for (int i=O; i<8; i++)
for (int j=O; j< 9; j+)
for (int k=O; k<8; k++)
//以指针形式访问数组元素
*(*(*(+.i. )+j)+k)=static cast< float> (i *100+ j *10+ k) ;

string

由于string 类具有接收const char 头类型的构造函数,因此字符串常量和用字符数纽表示的字符串变量都可以隐含地转换为stnng 对象。例如,可以直接使用字符串常量对stnng 对象初始化2
string str= “Hello world!”;
stnng 类的操作符:
s +t 将串s 和t 连接成一个新串
s!= t 判断s 与t 是否不等
string append (const char *s) ;将字符串s 添加在本串尾
string assign (const char *s) ;赋值,将s 所指向的字符串赋值给本对象
int compare (const string &str) const;
比较本串与str 中串的大小,当本串<str 串时,返回负数J 当本串>str 串时,返回正数J
两串相等时,返回0。
void swap (string& str) ;
//将本串与str 中的字符串进行交换
unsigned int find(const basic_string &str) const;
//查找并返回str 在本串中第一次出现的位置

基类与派生类

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有振生类都可以解决。类型兼容规则中所指的替代包括以下的情况。
·派生类的对象可以隐含转换为基类对象。
.派生类的对象可以初始化基类的引用。
·派生类的指针可以隐含转换为基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
如果B 类为基类, D 为B 类的公有派生类,则D 类中包含了基类B 中除构造、析构函数之外的所有成员。这时,根据类型兼容规则,在基类B 的对象可以出现的任何地方,都可以用派生类D 的对象来替代。
class B {…}
class D: public B {…}
B b1 ,*pb1;
D dl;
派生类的对象也可以初始化基类对象的引用:
B &rb=d1;
,可以将公有派生类对象的地址赋值给基类类型的指针,

定义了派生类之后,要使用派生类就需要声明该类的对象。对象在使用之前必须初始化。派生类的成员对象是由所有基类的成员对象与派生类新增的成员对象共同组成。
因此构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化基类的
构造函数并没有继承下来,要完成这些工作,就必须给派生类添加新的构造函数。派生类对于基类的很多成员对象是不能直接访问的,因此要完成对基类成员对象的初始化工作,需要通过调用基类的构造函数。派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用于初始化相应的成员,另一些参数要用于对派生类新增的成员对象进行初始化。**在构造派生类的对象时,会首先调用基类的构造函数,来初始化它们的数据成员,**然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员对象,最后才执行派生类构造函数的函数体。

基类构造函数的调用顺序是按照派生类定义时的顺序,因此应该是先Base2. 再Base1. 最后Base3;
而内嵌对象的构造函数调用顺序应该是按照成员在类中声明的顺序

虚基类

。在派生类的对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。可以使用作用域分辨符来唯一标识并分别访问它们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射。这样就解决了同名成员的唯一标识问题。

class 派生类名:virtual 继承方式基类名
class BaseO {
    
    
public:
int varO;

void funO () {
    
    cout<< "Member of BaseO"<< endl; }
}
class Basel: virtuaJ. public BaseO {
    
    
public:
int varl;
}

构造一个类的对象的一般顺序是:
(1)如果该类有直接或间接的虚基类,则先执行虚基类的构造函数。
(2) 如采该类有其他,基类,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但构造过程中,不再执行它们的虚基类的构造函数。
(3) 按照在类定义中出现的顺序,对派生类中新增的成员对象进行初始化。对于类类型的成员对象,如果出现在构造函数初始化列表中,则以其中指定的参数执行构造函数,如未出现,则执行默认构造函数;对于基本数据类型的成员对象,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋初值,否则什么也不做。
(4) 执行构造函数的函数体。

派生类指针可以隐含转换为基类指针,之所以允许这种转换隐含发生,是因为它是安全的转换。而派生类指针要想转换为基类指针,则转换一定要显式地进行。例如:
Base*pb= new Derived () ; / /将Derived 指针隐含转换为Base 指针
Derived *pd= static cast< Derived *> (pd); / /将Base 指针显式转换为Derived 指针

虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,问题是访问到的只是从基类继承来的同名成员。解决这一问题的办法是:如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以便属于不同派生类的不同对象产不同的行为,从而实现运行过程的多态。
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。而基类中声明的非虚函数,通常代表那些不希望被派生类改变的功能,也是不能实现多态的。一般不要重写继承而来的非虚函数(虽然语法对此没有强行限制) ,因为那会导致通过基类指针和派生类的指针或对象调用同名函数时,产生不同的结果,从而引起混乱。

,通过基类指针删除派牛类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成了内存泄漏。
也就是说派生类对象成员p 所指向的内存空间,在对象消失后既不能被本程序继续使用也没有被择放。对于内存需求量较大、长期连续运行的程序来说,如果持续发生这样的错误是很危险的,最终将导致因内存不足而引起程序终止。
避免上述错误的有效方法就是将析构函数声明为虚函数:

class Base (
public:
virtual - Base () ;}
纯虚函数的声明格式为2

virtual 函数类型函数名(参数表)=0
实际上,它与一般虚函数成员的原型在书写格式上的不同就在于后面加了"= 0" 。
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。纯虚函数的函数体由派生类给出。

基类指针指向子类对象时的类型转化

基类的指针可以指向派生类的对象,通过这样的指针虽然可以利用多态性来执行派生类提供的功能,但这仅限于调用基类中声明的虚函数。如果希望对于一部分派生类的对象,调用派生类中引人的新函数,则无法通过基类指针进行
dynamic_cast 是与static_cast. const_cast , reinterpret_cast 并列的4 种类型转换操作符之→。它可以将基类的指针显式转换为派生类的指针,或将基类的引用显式转换为派生类的引用。{!3.与static_cast 不同的是,它执行的不是无条件的转换,它在转换前会检查指针(或引用)所指向对象的实际类型是否与转换的日的类型兼容,如果兼容转换才会发生,才能得到派生类的指针(或引用) .再则:
·如果执行的是指针类型的转换,会得到空指针。
·如果执行的是引用类型的转换,会抛出异常
示例:

class Base{
    
    
public:
virtual void funl () {
    
    cout<< "Base: : fun1 () "<< endl;}
}
class Derived1: public Base {
    
    
public:
virtual void fun1 () {
    
    cout<< "Derivedl : : funl () "<< endl; }
virtual void fun2 () {
    
    cout<< "Derived1: :fun2 () "<<endl;}
}
void fun (Base *b) {
    
    
b- > funl ();
Derived1 * d= dnamic_cast<Derived1*> (b) ; //尝试将b 转换为Derived1 指针
if (d!=O) d->fun2(); //判断转换是否成功

由于fun1 函数是基类Base 中定义的函数,通过Base 类的指针b 可以直接调用fun1()函数。fun2函数是派生类Derived1 中引人的新函数,只能对Derived1类的对象调用。

用typeid 获取运行时类型信息

typeid 是c++ 的一个关键字,用它可以获得一个类型的相关信息。

猜你喜欢

转载自blog.csdn.net/qq_41358574/article/details/111413318