C++学习笔记——1

目录

端口

网络字节序

套接字

基于TCP(面向连接)的socket编程过程

Const

Const初始化

运算符重载

拷贝构造函数

返回值为引用的情况

C++中基类的析构函数为什么要用virtual虚析构函数

vector容器begin()与end()函数、front()与back()的用法

C语言#pragma使用方法

【c++】size_t和size_type的区别

网络编程

1、端口

16位长度的数字,0~65535,编号1024以下的保留给预定义的服务。编写网络应用程序时,应使用1024以上的端口号。
回到目录

2、网络字节序

TCP/IP协议使用大端序(高位先存)
回到目录

3、套接字

  • 套接字类型

SOCK_STREAM 流式套接字:基于TCP协议实现的,提供有连接、可靠的数据传输服务,数据无差错、无重复的发送,且按照发送顺序接收。
SOCK_DGRAM 数据报式套接字(datagram 数据报):基于UDP协议实现的,提供无连接的服务,数据包以独立的形式发送,不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。
SOCK_RAW 元式套接字
回到目录

4、基于TCP(面向连接)的socket编程过程

1)服务器端:

  • 创建套接字(socket)
  • 将套接字绑定到一个本地地址和端口上(bind)
  • 将套接字设置为监听模式,准备接受客户端的请求(listen)
  • 等待客户端请求到来;当请求到来后,返回一个新的对应于此次连接的套接字(accept)
    回到目录

5、Const

为什么使用const?采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替),分类如下:

常变量: const 类型说明符 变量名

常引用: const 类型说明符 &引用名

常对象: 类名 const 对象名

常成员函数: 类名::fun(形参) const

常数组: 类型说明符 const 数组名[大小]

常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名

首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:

 const int a=5; 与 int const a=5; 等同

 类名 const 对象名 与 const 类名 对象名 等同

用法1:常量
取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
C标准中,const定义的常量是全局的,C++中视声明位置而定。

用法2:指针和常量
使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
所以出现在 * 之前的const是作为基础类型的一部分:
char *const cp; //到char的const指针
char const *pc1; //到const char的指针
const char pc2; //到const char的指针(后两个声明是等同的)
从右向左读的记忆方式:
cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
pc2 is a pointer to const char. 故
pc2的内容不可以改变,但pc2可以指向别的字符串

且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。

用法3:const修饰函数传入参数
将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数

用法4:修饰函数返回值
可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。

用法5:const修饰成员函数(c++特性)
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

具体展开来讲:
(一). 常量与指针

常量与指针放在一起很容易让人迷糊。对于常量指针和指针常量也不是所有的学习C/C++的人都能说清除。例如:

const int *m1 = new int(10);

int* const m2 = new int(20);

在上面的两个表达式中,最容易让人迷惑的是const到底是修饰指针还是指针指向的内存区域?其实,只要知道:const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。根据这个规则来判断,m1应该是常量指针(即,不能通过m1来修改它所指向的内容。);而m2应该是指针常量(即,不能让m2指向其他的内存模块)。由此可见:

  1. 对于常量指针,不能通过该指针来改变所指的内容。即,下面的操作是错误的:

    int i = 10;

    const int *pi = &i;

    *pi = 100;

    因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:

    // 1: 通过i直接修改。

    i = 100;

    // 2: 使用另外一个指针来修改。

    int p = (int)pi;

    *p = 100;

    实际上,在将程序载入内存的时候,会有专门的一块内存区域来存放常量。但是,上面的i本身不是常量,是存放在栈或者堆中的。我们仍然可以修改它的值。而pi不能修改指向的值应该说是编译器的一个限制。

  2. 根据上面const的规则,const int *m1 = new int(10);我们也可写作:

    int const *m1 = new int(10);

    这是,理由就不须作过多说明了。

  3. 在函数参数中指针常量时表示不允许将该指针指向其他内容。

    void func_02(int* const p)

    {

    int *pi = new int(100);

    //错误!P是指针常量。不能对它赋值。

    p = pi;

    }

    int main()

    {

    int* p = new int(10);

    func_02§;

    delete p;

    return 0;

    }

  4. 在函数参数中使用常量指针时表示在函数中不能改变指针所指向的内容。

void func(const int *pi)

{

//错误!不能通过pi去改变pi所指向的内容!

*pi = 100;

}

int main()

{

int* p = new int(10);

func(p); 

delete p;

return 0;

}

我们可以使用这样的方法来防止函数调用者改变参数的值。但是,这样的限制是有限的,作为参数调用者,我们也不要试图去改变参数中的值。因此,下面的操作是在语法上是正确的,但是可能破还参数的值:

#include <iostream>

#include <string>

void func(const int *pi)

{

//这里相当于重新构建了一个指针,指向相同的内存区域。当然就可以通过该指针修改内存中的值了。

int* pp = (int*)pi;

*pp = 100;

}

int main()

{

using namespace std;

int* p = new int(10);

cout << "*p = " << *p << endl;

func(p);

cout << "*p = " << *p << endl;

delete p;

return 0;

}

(二):常量与引用

常量与引用的关系稍微简单一点。因为引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名, 那么他们只剩下代表的内存区域是否可变。即:

int i = 10;

// 正确:表示不能通过该引用去修改对应的内存的内容。

const int& ri = i;

// 错误!不能这样写。

int& const rci = i;

由此可见,如果我们不希望函数的调用者改变参数的值。最可靠的方法应该是使用引用。下面的操作会存在编译错误:

void func(const int& i)

{

// 错误!不能通过i去改变它所代表的内存区域。

i = 100;

}

int main()

{

int i = 10;

func(i);

return 0;

}

这里已经明白了常量与指针以及常量与引用的关系。但是,有必要深入的说明以下。在系统加载程序的时候,系统会将内存分为4个区域:堆区 栈区全局区(静态)和代码区。从这里可以看出,对于常量来说,系统没有划定专门的区域来保护其中的数据不能被更改。也就是说,使用常量的方式对数据进行保护是通过编译器作语法限制来实现的。我们仍然可以绕过编译器的限制去修改被定义为“常量”的内存区域。看下面的代码:

const int i = 10;

// 这里i已经被定义为常量,但是我们仍然可以通过另外的方式去修改它的值。

// 这说明把i定义为常量,实际上是防止通过i去修改所代表的内存。

int *pi = (int*) &i;

(三):常量函数

常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察着);另一类是非常量成员函数(也被成为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。例如:

class Test

{

public:

void func() const;

private:

int intValue;

};

void Test::func() const

{

intValue = 100;

}

上面的代码中,常量函数func函数内试图去改变数据成员intValue的值,因此将在编译的时候引发异常。

当然,对于非常量的成员函数,我们可以根据需要读取或修改数据成员的值。但是,这要依赖调用函数的对象是否是常量。通常,如果我们把一个类定义为常量,我们的本意是希望他的状态(数据成员)不会被改变。那么,如果一个常量的对象调用它的非常量函数会产生什么后果呢?看下面的代码:

class Fred{

public:

void inspect() const;

void mutate();

};

void UserCode(Fred& changeable, const Fred& unChangeable)

{

changeable.inspect(); // 正确,非常量对象可以调用常量函数。

changeable.mutate(); // 正确,非常量对象也允许修改调用非常量成员函数修改数据成员。

unChangeable.inspect(); // 正确,常量对象只能调用常理函数。因为不希望修改对象状态。

unChangeable.mutate(); // 错误!常量对象的状态不能被修改,而非常量函数存在修改对象状态的可能

}

从上面的代码可以看出,由于常量对象的状态不允许被修改,因此,通过常量对象调用非常量函数时将会产生语法错误。实际上,我们知道每个成员函数都有一个隐含的指向对象本身的this指针。而常量函数则包含一个this的常量指针。如下:

void inspect(const Fred* this) const;

void mutate(Fred* this);

 也就是说对于常量函数,我们不能通过this指针去修改对象对应的内存块。但是,在上面我们已经知道,这仅仅是编译器的限制,我们仍然可以绕过编译器的限制,去改变对象的状态。看下面的代码:

class Fred{

public:

void inspect() const;
private:

int intValue;

};

void Fred::inspect() const

{

cout << "At the beginning. intValue = "<< intValue << endl;

// 这里,我们根据this指针重新定义了一个指向同一块内存地址的指针。

// 通过这个新定义的指针,我们仍然可以修改对象的状态。

Fred* pFred = (Fred*)this;

pFred->intValue = 50;

cout << "Fred::inspect() called. intValue = "<< intValue << endl;

}

int main()

{

Fred fred;

fred.inspect();

return 0;

}

上面的代码说明,只要我们愿意,我们还是可以通过常量函数修改对象的状态。同理,对于常量对象,我们也可以构造另外一个指向同一块内存的指针去修改它的状态。这里就不作过多描述了。

另外,也有这样的情况,虽然我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。例如:

class Fred{

public:

void inspect() const;

private:

mutable int intValue;

};

void Fred::inspect() const

{

intValue = 100;

}

但是,并不是所有的编译器都支持mutable关键字。这个时候我们上面的歪门邪道就有用了。

关于常量函数,还有一个问题是重载。

#include <iostream>

#include <string>

using namespace std;

class Fred{

public:

void func() const;

void func();

};

void Fred::func() const

{

cout << "const function is called."<< endl;

}

void Fred::func()

{

cout << "non-const function is called."<< endl;

}

void UserCode(Fred& fred, const Fred& cFred)

{

cout << "fred is non-const object, and the result of fred.func() is:" << endl;

fred.func();

cout << "cFred is const object, and the result of cFred.func() is:" << endl;

cFred.func();

}

int main()

{

Fred fred;

UserCode(fred, fred);

return 0;

}

输出结果为:

fred is non-const object, and the result of fred.func() is:

non-const function is called.

cFred is const object, and the result of cFred.func() is:

const function is called.

从上面的输出结果,我们可以看出。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。

总之,我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的函数。这样的规定虽然很武断,但如果我们都根据原则去编写或使用类的话这样的规定也就完全可以理解了。

(四):常量返回值

 很多时候,我们的函数中会返回一个地址或者引用。调用这得到这个返回的地址或者引用后就可以修改所指向或者代表的对象。这个时候如果我们不希望这个函数的调用这修改这个返回的内容,就应该返回一个常量。这应该很好理解,大家可以去试试。

+++++++++++++++++++++++++++++++++++++++

c++ 中const

+++++++++++++++++++++++++++++++++++++++

  1. const常量,如const int max = 100;
    优点:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)

  2. const 修饰类的数据成员。如:
    class A
    {

    const int size;

}

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。如

class A

{

const int size = 100; //错误

int array[size]; //错误,未知的size

}
const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如

class A

{…

enum {size1=100, size2 = 200 };

int array1[size1];

int array2[size2];

}

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

  1. const修饰指针的情况,见下式:

int b = 500;
const int* a = & [1]
int const a = & [2]
int
const a = & [3]
const int* const a = & [4]

如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effectivec++》Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。

  1. const的初始化

先看一下const变量初始化的情况

  1. 非指针const常量初始化的情况:A b;
    const A a = b;

  2. 指针const常量初始化的情况:

A* d = new A();
const A* c = d;
或者:const A* c = new A();
3)引用const常量初始化的情况:
A f;
const A& e = f; // 这样作e只能访问声明为const的函数,而不能访问一般的成员函数;

[思考1]: 以下的这种赋值方法正确吗? 
const A* c=new A(); 
A* e = c; 
[思考2]: 以下的这种赋值方法正确吗? 
A* const c = new A(); 
A* b = c;
  1. 另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:A&operator=(const A& a);
    void fun0(const A* a );
    void fun1( ) const; // fun1( ) 为类成员函数
    const A fun2( );
  1. 修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
    调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
    [注意]:参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。

[总结]对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a)

  对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改为void Func(const int &x)
  1. 修饰返回值的const,如const A fun2( ); const A* fun3( );
    这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
    return Rational(lhs.numerator() * rhs.numerator(),
    lhs.denominator() * rhs.denominator());
    }

返回值用const修饰可以防止允许这样的操作发生:Rational a,b;
Radional c;
(a*b) = c;

一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
[总结]

  1. 一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A实例)或某个对象的引用为const(const A& test = A实例),则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

  2. 如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。如:

const char * GetString(void);

如下语句将出现编译错误:

char *str=GetString();

正确的用法是:

const char *str=GetString();

  1. 函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赙值函数中,目的是为了实现链式表达。如:
    

class A

{…

A &operate = (const A &other); //负值函数

}
A a,b,c; //a,b,c为A的对象

a=b=c; //正常

(a=b)=c; //不正常,但是合法

若负值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
[思考3]: 这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);

  1. 类成员函数中const的使用 
    

一般放在函数体后,形如:void fun() const;
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:

class Stack

{

public:

  void Push(int elem);

  int Pop(void);

  int GetCount(void) const;   //const 成员函数

private:

  int m_num;

  int m_data[100];

};

int Stack::GetCount(void) const

{

++m_num; //编译错误,企图修改数据成员m_num

Pop(); //编译错误,企图调用非const函数

Return m_num;

}

  1. 使用const的一些建议
  1. 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
  2. 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
  3. 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
  4. const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
  5. 不要轻易的将函数的返回值类型定为const;
  6. 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;

[思考题答案]

  1. 这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
  2. 这种方法正确,因为声明指针所指向的内容可变;
  3. 这种做法不正确;
    在const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
    A a,b,c:
    (a=b)=c;
    因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。

++++++++++++++++++++++++++++++++++++++++

const 在c和c++中的区别 http://tech.e800.com.cn/articles/2009/722/1248229886744_1.html

++++++++++++++++++++++++++++++++++++++++

  1. C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中.所以,以下代码:
    using namespace std;
    int main()
    {
    const int a = 1;
    const int b = 2;
    int array[ a + b ] = {0};
    for (int i = 0; i < sizeof array / sizeof *array; i++)
    {
    cout << array << endl;
    }
    }
    在可以通过编译,并且正常运行.但稍加修改后,放在C编译器中,便会出现错误:
    int main()
    {
    int i;
    const int a = 1;
    const int b = 2;
    int array[ a + b ] = {0};
    for (i = 0; i < sizeof array / sizeof *array; i++)
    {
    printf("%d",array);
    }
    }
    错误消息:
    c:\test1\te.c(8): error C2057: 应输入常数表达式
    c:\test1\te.c(8): error C2466: 不能分配常数大小为 0 的数组
    出现这种情况的原因是:在C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值.而且,数组定义时的下标必须为常量.
  2. 在C语言中: const int size; 这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的.C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.即C++中,const默认使用内部连接.而C中使用外部连接.
    (1) 内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定.
    (2) 外连接:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数.
    /* C++代码 header.h /
    const int test = 1;
    /
    C++代码 test1.cpp /
    #include “header.h”
    using namespace std;
    int main() { cout << “in test1 :” << test << endl; }
    /
    C++代码 test2.cpp /
    #include “header.h”
    using namespace std;
    void print() { cout << “in test2:” << test << endl;}
    以上代码编译连接完全不会出问题,但如果把header.h改为:
    extern const int test = 1;
    在连接的时候,便会出现以下错误信息:
    test2 error LNK2005: “int const test” (?test@@3HB) 已经在 test1.obj 中定义
    因为extern关键字告诉C++编译器test会在其他地方引用,所以,C++编译器就会为test创建存储空间,不再是简单的存储在名字表里面.所以,当两个文件同时包含header.h的时候,会发生名字上的冲突.
    此种情况和C中const含义相似:
    /
    C代码 header.h /
    const int test = 1;
    /
    C代码 test1.c /
    #include “header.h”
    int main() { printf(“in test1:%d\n”,test); }
    /
    C代码 test2.c */
    #include “header.h”
    void print() { printf(“in test2:%d\n”,test); }
    错误消息:
    test3 fatal error LNK1169: 找到一个或多个多重定义的符号
    test3 error LNK2005: _test 已经在 test1.obj 中定义

也就是说:在c++ 中const 对象默认为文件的局部变量。与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象:
// file_1.cc
// defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_2.cc
extern const int bufSize; // uses bufSize from file_1
// uses bufSize defined in file_1
for (int index = 0; index != bufSize; ++index)
// …

  1. C++中,是否为const分配空间要看具体情况.如果加上关键字extern或者取const变量地址,则编译器就要为const分配存储空间.
  2. C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.
    回到目录

5、Const初始化

Const成员如其它任何成员一样,简单考虑其出现在三个位置:全局作用域、普通函数内部、类里面。

下面请看测试代码:

#include<iostream>
 using namespace std;
 
 class A
 {
 public:
     const int j;
//     const int k = 3;
     const static int i;
 };
 
 //const static int A::i = 0;
 const int A::i = 1;
 
 //const int j;
 int main(int argc, char* argv[])
 {
     //const int i;
     cout << A::i << endl;
     system("pause");
     return 0;
 }

得出以下结论:
1、const常量在全局作用域声明时必须初始化,否则无法通过编译。

2、const常量在普通函数中(此处为main函数)中一样,也必须在声明时就进行初始化。

3、在类(此处为A)定义体中,从const常量j可以看出,不用初始化也能通过声明。有特殊原因,从const常量 k可以看出,不能在类定义体中进行初始话(原因见5)。

4、同理const static也可以在类定义体中声明,而在类外部初始化(此处为const static int i)。但注意,此处的i可以在类定义体中进行初始化。

5、C++规定,只有“const static的整形成员”才可以在类定义体中进行初始化。

以上转自:http://www.cnblogs.com/yaohwang/archive/2012/03/03/2367983.html

对于j的初始化如下:

#include<iostream>
 using namespace std;
 
 class A
 {
 public:
     A();
     const int j;
//     const int k = 3;
     const static int i;
 };
 
 //const static int A::i = 0;
 const int A::i = 1;
 A::A():j(3){}
 //const int j;
 int main(int argc, char* argv[])
 {
     //const int i;
     cout << A::i << endl;
     A a;
     cout<<a.j<<endl;
     system("pause");
     return 0;
 }

输出是

1

3

const int j可以在构造函数初始化表达式表中进行初始化,但是不能在构造函数体内进行初始化。
回到目录

6、运算符重载

当我们没有自己设计等号运算符的重载函数,编译器会自动生成一个浅拷贝的赋值运算符的重载函数。

浅拷贝:只是简单地将一个对象的内存数据赋值给另一个对象,如果这个对象成员变量引用了外部资源时(new),那么这两个对象的成员变量都指向这个空间,当这两个对象生存周期结束时,进行析构,那么就会崩溃,对同一块内存我们delete了两次。

思路:

①防止自赋值,判断this指针和参数&good是不是相等,

②判断this的成员变量(有申请外部资源的)中的是否为NULL,if not ,delete该外部资源,

③重新申请,并且用good对其进行赋值。

正文:

Ⅰ.举例

例1

复制代码
#include
#include
using namespace std;

class MyStr
{
private:
char *name;
int id;
public:
MyStr() {}
MyStr(int _id, char *_name) //constructor
{
cout << “constructor” << endl;
id = _id;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1, _name);
}
MyStr(const MyStr& str)
{
cout << “copy constructor” << endl;
id = str.id;
if (name != NULL)
delete[] name;
name = new char[strlen(str.name) + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
MyStr& operator =(const MyStr& str)//赋值运算符
{
cout << “operator =” << endl;
if (this != &str)
{
if (name != NULL)
delete[] name;
this->id = str.id;
int len = strlen(str.name);
name = new char[len + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
return *this;
}
~MyStr()
{
delete[] name;
}
};

int main()
{
MyStr str1(1, “hhxx”);
cout << “" << endl;
MyStr str2;
str2 = str1;
cout << "
” << endl;
MyStr str3 = str2;
return 0;
}
复制代码
结果:

Ⅱ.参数

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是因为:

①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。

②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。

用引用是因为:

这样可以避免在函数调用时对实参的一次拷贝,提高了效率。

注意:

上面的规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象,正如后面例2中的那样。

Ⅲ.返回值

一般地,返回值是被赋值者的引用,即*this(如上面例1),原因是

①这样在函数返回时避免一次拷贝,提高了效率。

②更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。

注意:

这也不是强制的,我们可以将函数返回值声明为void,然后什么也不返回,只不过这样就不能够连续赋值了。

Ⅳ.调用时机

  当为一个类对象赋值(注意:可以用本类对象为其赋值(如上面例1),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。

如上边代码中

str2 = str1;

一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。

需要注意的是,

MyStr str2;

str2 = str1;

MyStr str3 = str2;

在调用函数上是有区别的。正如我们在上面结果中看到的那样。

  前者MyStr str2;一句是str2的声明加定义,调用无参构造函数,所以str2 = str1;一句是在str2已经存在的情况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;而后者,是用str2来初始化str3,调用的是拷贝构造函数。

Ⅴ.提供默认赋值运算符重载函数的时机

  当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。

见下面的例2

复制代码
#include
#include
using namespace std;

class Data
{
private:
int data;
public:
Data() {};
Data(int _data)
:data(_data)
{
cout << “constructor” << endl;
}
Data& operator=(const int _data)
{
cout << “operator=(int _data)” << endl;
data = _data;
return *this;
}
};

int main()
{
Data data1(1);
Data data2,data3;
cout << “=" << endl;
data2 = 1;
cout << "
=” << endl;
data3 = data2;
return 0;
}
复制代码
结果:

 上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。

Ⅵ.构造函数还是赋值运算符重载函数

 如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。只不过结论变成了

可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时

①如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。

②如果只有匹配的构造函数存在,就会调用这个构造函数。

Ⅶ.显式提供赋值运算符重载函数的时机

①用非类A类型的值为类A的对象赋值时(当然,从Ⅵ中可以看出,这种情况下我们可以不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。

②当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝(关于浅拷贝和深拷贝,下面会讲到),必须显式提供赋值运算符重载函数(如例1)。

Ⅷ.浅拷贝和深拷贝

  拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。

  所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。

  假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2;语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图

                                               

 这样,会有两个致命的错误

①当我们通过str2修改它的name时,str3的name也会被修改!

②当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!

  这是万万不可行的,所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,delete[] name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图

                                                

  这样,str2.name和str3.name各自独立,避免了上面两个致命错误。

  我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。

Ⅸ.赋值运算符重载函数只能是类的非静态的成员函数

   C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于原因,有人说,赋值运算符重载函数往往要返回*this,而无论是静态成员函数还是友元函数都没有this指针。这乍看起来很有道理,但仔细一想,我们完全可以写出这样的代码

static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{
……
return str1;
}
可见,这种说法并不能揭露C++这么规定的原因。

  其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。

  在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?

   为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。

Ⅹ. 赋值运算符重载函数不能被继承

见下面的例3

复制代码
#include
#include
using namespace std;

class A
{
public:
int X;
A() {}
A& operator =(const int x)
{
X = x;
return *this;
}
};
class B :public A
{
public:
B(void) :A() {}
};

int main()
{
A a;
B b;
a = 45;
//b = 67;
(A)b = 67;
return 0;
}
复制代码
注释掉的一句无法编译通过。报错提示:没有与这些操作数匹配的”=”运算符。对于b = 67;一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,类似于该句的代码可以调用匹配的构造函数),此时,代码不能编译通过,说明父类的operator =函数并没有被子类继承。

 为什么赋值运算符重载函数不能被继承呢?

 因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?

 所以,C++规定,赋值运算符重载函数不能被继承。

上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。

Ⅺ.赋值运算符重载函数要避免自赋值

  对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(正如例1中的if (this != &str)一句)。

 为什么要避免自赋值呢?

①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

②如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?

  所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。

结束语:

  至此,本文的所有内容都介绍完了。由于在下才疏学浅,错误纰漏之处在所难免,如果您在阅读的过程中发现了在下的错误和不足,请您务必指出。您的批评指正就是在下前进的不竭动力! 

回到目录

7、拷贝构造函数

一、什么是拷贝构造函数

  首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a=100;
int b=a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

复制代码
复制代码
#include
using namespace std;
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a=b;
printf(“constructor is called\n”);
}
//拷贝构造函数
CExample(const CExample & c)
{
a=c.a;
printf(“copy constructor is called\n”);
}
//析构函数
~CExample()
{
cout<<“destructor is called\n”;
}
void Show()
{
cout<<a<<endl;
}
};
int main()
{
CExample A(100);
CExample B=A;
B.Show();
return 0;
}
复制代码
复制代码
运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。

二、拷贝构造函数的调用时机
1. 当函数的参数为类的对象时

复制代码
复制代码

#include
using namespace std;
class CExample
{
private:
int a;
public:
CExample(int b)
{
a=b;
printf(“constructor is called\n”);
}
CExample(const CExample & c)
{
a=c.a;
printf(“copy constructor is called\n”);
}
~CExample()
{
cout<<“destructor is called\n”;
}
void Show()
{
cout<<a<<endl;
}
};
void g_fun(CExample c)
{
cout<<“g_func”<<endl;
}
int main()
{
CExample A(100);
CExample B=A;
B.Show();
g_fun(A);
return 0;
}
复制代码
复制代码

调用g_fun()时,会产生以下几个重要步骤:
(1).A对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把A的值给C。 整个这两个步骤有点像:CExample C(A);
(3).等g_fun()执行完后, 析构掉 C 对象。

  1. 函数的返回值是类的对象

复制代码
复制代码
#include
using namespace std;
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a=b;
printf(“constructor is called\n”);
}
//拷贝构造函数
CExample(const CExample & c)
{
a=c.a;
printf(“copy constructor is called\n”);
}
//析构函数
~CExample()
{
cout<<“destructor is called\n”;
}
void Show()
{
cout<<a<<endl;
}
};
CExample g_fun()
{
CExample temp(0);
return temp;
}
int main()
{

g_fun();
return 0;

}
复制代码
复制代码

当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_fun()执行完后再析构掉XXXX对象。

  1. 对象需要通过另外一个对象进行初始化
    CExample A(100);
    CExample B=A;
    三、浅拷贝与深拷贝
    1. 默认拷贝构造函数
      很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
      Rect::Rect(const Rect& r)
      {
      width=r.width;
      height=r.height;
      }
      当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

复制代码
复制代码

#include
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
~Rect()
{
count–;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<“The count of Rect:”<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<“The count of Rect:”<<Rect::getCount()<<endl;
return 0;
}

复制代码
复制代码
这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

说白了,就是拷贝构造函数没有处理静态数据成员。

出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:

复制代码
复制代码
#include
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
Rect(const Rect& r)
{
width=r.width;
height=r.height;
count++;
}
~Rect()
{
count–;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<“The count of Rect:”<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<“The count of Rect:”<<Rect::getCount()<<endl;
return 0;
}
复制代码
复制代码

  1. 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:

复制代码
复制代码
#include
#include<assert.h>
using namespace std;
class Rect
{
public:
Rect()
{
p=new int(100);
}

~Rect()
{
 assert(p!=NULL);
    delete p;
}

private:
int width;
int height;
int *p;
};
int main()
{
Rect rect1;
Rect rect2(rect1);
return 0;
}
复制代码
复制代码

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:

当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

  1. 深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

复制代码
复制代码
#include
#include<assert.h>
using namespace std;
class Rect
{
public:
Rect()
{
p=new int(100);
}

Rect(const Rect& r)
{
 width=r.width;
    height=r.height;
 p=new int(100);
    *p=*(r.p);
}
 
~Rect()
{
 assert(p!=NULL);
    delete p;
}

private:
int width;
int height;
int *p;
};
int main()
{
Rect rect1;
Rect rect2(rect1);
return 0;
}
复制代码
复制代码

此时,在完成对象的复制后,内存的一个大致情况如下:

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。
3. 防止默认拷贝发生

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

复制代码
复制代码

//防止按值传递
class CExample
{
private:
int a;

public:
//构造函数
CExample(int b)
{
a = b;
cout<<"creat: "<<a<<endl;
}

private:
//拷贝构造函数,只是声明
CExample(const CExample& C);

public:
~CExample()
{
cout<< "delete: "<<a<<endl;
}

void Show () 
{ 
    cout<<a<<endl; 
} 

};

//???
void g_Fun(CExample C)
{
cout<<“test”<<endl;
}

int main()
{
CExample test(1);
//g_Fun(test); //按值传递将出错

return 0; 

}
复制代码
复制代码
小结:
拷贝有两种:深拷贝,浅拷贝。
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。

 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

四、拷贝构造函数的几个细节
1.为什么拷贝构造函数必须是引用传递,不能是值传递?
简单的回答是为了防止递归引用。
具体一些可以这么讲:
当 一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当 需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参; 而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归。

  1. 拷贝构造函数的作用。
    作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
 3.参数传递过程到底发生了什么?
  将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!
  i)值传递:
     对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
     对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj);  首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
 ii)引用传递:
    无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).
  1. 在类中有指针数据成员时,拷贝构造函数的使用?
    如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题 了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。

问题1和2回答了为什么拷贝构造函数使用值传递会产生无限递归调用的问题;
问题3回答了回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,因为默认的拷贝构造函数就是这么干的。

  1. 拷贝构造函数里能调用private成员变量吗?
    解答:这个问题是在网上见的,当时一下子有点晕。其时从名子我们就知道拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。

  2. 以下函数哪个是拷贝构造函数,为什么?

X::X(const X&); //拷贝构造函数
X::X(X);
X::X(X&, int a=1); //拷贝构造函数
X::X(X&, int a=1, int b=2); //拷贝构造函数
解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

  1. 一个类中可以存在多于一个的拷贝构造函数吗?
    解答:类中可以存在超过一个拷贝构造函数。

class X {
public:
X(const X&); // const 的拷贝构造
X(X&); // 非const的拷贝构造
};

注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

五、C++构造函数以及析构函数的若干面试问题

Q1:构造函数能否重载,析构函数能否重载,为什么?
A1:构造函数可以,析构函数不可以。

Q2:析构函数为什么一般情况下要声明为虚函数?
A2:虚函数是实现多态的基础,当我们通过基类的指针是析构子类对象时候,如果不定义成虚函数,那只调用基类的析构函数,子类的析构函数将不会被调用。如 果定义为虚函数,则子类父类的析构函数都会被调用。
Q3:什么情况下必须定义拷贝构造函数?
A3:当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆 栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。

参考博客:
http://blog.csdn.net/lwbeyond/article/details/6202256
http://jaden.blog.51cto.com/1549175/324480
http://blog.chinaunix.net/uid-28662931-id-3496322.html

回到目录

9、返回值为引用的情况

1)函数返回值用引用,参数传递进去也用引用:

int& hel(int& t){
    return t;
}

2)函数返回值用引用,参数传递用指针

int& hel(int *t){
    return *t;
}

3)返回函数内部的数据:

copy assignment运算符

class A{
public:
    A(int xx) :x(xx){
    }
    A(){

    }
    int getX() const{
        return x;
    };
    void show(){
        cout << x << endl;
    }
    A(const A&a){
        this->x = a.x;
    }
    A& operator=(const A& a){
        this->x = a.x;
        return *this;
    }
private:
    int x;
};

内部数据

class Point{
public:
    Point(int x,int y);
    ...
    void setX(int newVal);
    void setY(int newVal);
    ...
};
struct RectData{
    Point ulhc;
    Point lrhc;
};
class Rectangle{
public:
    ...
    Point& upperLeft()const 
    {
    return pData->ulhc;
    }
    Point& lowerRight()const{
    return pData->lrhc;
    }
private:
    Rectangle pData;
};

回到目录

10、C++中基类的析构函数为什么要用virtual虚析构函数

知识背景
要弄明白这个问题,首先要了解下C++中的动态绑定。

     关于动态绑定的讲解,请参阅:  C++中的动态类型与动态绑定、虚函数、多态实现

正题
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

示例代码讲解
现有Base基类,其析构函数为非虚析构函数。Derived1和Derived2为Base的派生类,这两个派生类中均有以string* 指向存储其name的地址空间,name对象是通过new创建在堆上的对象,因此在析构时,需要显式调用delete删除指针归还内存,否则就会造成内存泄漏。

class Base {
public:
~Base() {
cout << “~Base()” << endl;
}
};
复制代码
class Derived1 : public Base {
public:
Derived1():name_(new string(“NULL”)) {}
Derived1(const string& n):name_(new string(n)) {}

~Derived1() {
delete name_;
cout << “~Derived1(): name_ has been deleted.” << endl;
}

private:
string* name_;
};

class Derived2 : public Base {
public:
Derived2():name_(new string(“NULL”)) {}
Derived2(const string& n):name_(new string(n)) {}

~Derived2() {
delete name_;
cout << “~Derived2(): name_ has been deleted.” << endl;
}

private:
string* name_;
};
复制代码
我们看下面对其析构情况进行测试:

int main() {
Derived1* d1 = new Derived1();
Derived2 d2 = Derived2(“Bob”);
delete d1;
return 0;
}
d1为Derived1类的指针,它指向一个在堆上创建的Derived1的对象;d2为一个在栈上创建的对象。其中d1所指的对象需要我们显式的用delete调用其析构函数;d2对象在其生命周期结束时,系统会自动调用其析构函数。看下其运行结果:

刚才我们说,Base基类的析构函数并不是虚析构函数,现在结果显示,派生类的析构函数被调用了,正常的释放了其申请的内存资源。这两者并不矛盾,因为无论是d1还是d2,两者都属于静态绑定,而且其静态类型恰好都是派生类,因此,在析构的时候,即使基类的析构函数为非虚析构函数,也会调用相应派生类的析构函数。

下面我们来看下,当发生动态绑定时,也就是当用基类指针指向派生类,这时候采用delete显式删除指针所指对象时,如果Base基类的析构函数没有virtual,会发生什么情况?

复制代码
int main() {
Base* base[2] = {
new Derived1(),
new Derived2(“Bob”)
};
for (int i = 0; i != 2; ++i) {
delete base[i];
}
return 0;
}
复制代码

    从上面结果我们看到,尽管派生类中定义了析构函数来释放其申请的资源,但是并没有得到调用。原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。

    也就是说,在基类的析构函数为非虚析构函数的时候,并不一定会造成内存泄漏;当派生类对象的析构函数中有内存需要收回,并且在编程过程中采用了基类指针指向派生类对象,如为了实现多态,并且通过基类指针将该对象销毁,这时,就会因为基类的析构函数为非虚析构函数而不触发动态绑定,从而没有调用派生类的析构函数而导致内存泄漏。

    因此,为了防止这种情况下内存泄漏的发生,最好将基类的析构函数写成virtual虚析构函数。

下面把Base基类的析构函数改为虚析构函数:

class Base {
public:
virtual ~Base() {
cout << “~Base()” << endl;
}
};
再看下其运行结果:

这样就会实现动态绑定,派生类的析构函数就会得到调用,从而避免了内存泄漏。

回到目录

11、vector容器begin()与end()函数、front()与back()的用法

一、begin函数

函数原型:

iterator begin();

const_iterator begin();

功能:

返回一个当前vector容器中起始元素的迭代器。

二、end函数

函数原型:

iterator end();

const_iterator end();

功能:

返回一个当前vector容器中末尾元素的迭代器。

三、front函数

函数原型:

reference front();

const_reference front();

功能:

返回当前vector容器中起始元素的引用。

四、back函数

函数原型:

reference back();

const_reference back();

功能:

返回当前vector容器中末尾元素的引用。

实例:

#include
#include
using namespace std;
int main()
{
vector v1;
vector::iterator iter1;
vector::iterator iter2;
v1.push_back(‘m’);
v1.push_back(‘n’);
v1.push_back(‘o’);
v1.push_back(‘p’);

cout << "v1.front() = " << v1.front() << endl;
cout << "v1.back() = " << v1.back() << endl;

iter1 = v1.begin();
cout << *iter1 << endl;
iter2 = v1.end()-1; //注意v1.end()指向的是最后一个元素的下一个位置,所以访问最后一个元素
//的正确操作为:v1.end() - 1;
cout << *iter2 << endl;
return 0;
}

输出结果:
v1.front() = m
v1.back() = p
m
p

回到目录

12、C语言#pragma使用方法

一、总结
1、#pragma用于指示编译器完成一些特定的动作

   2、#pragma所定义的很多指示字是编译器特有的(每种编译可能都不一样)

          (1) #pragma message 用于自定义编译信息

          (2)#pragma once 用于保证头文件只被编译一次

          (3)#pragama pack用于指定内存对齐(一般用在结构体)

                    struct占用内存大小

                        1)第一个成员起始于0偏移处

                        2)每个成员按其类型大小和pack参数中较小的一个进行对齐

                                 ——偏移地址必须能被对齐参数整除

                                 ——结构体成员的对齐参数(注意是对齐参数,而不是结构体长度)取其内部长度最大的数据成员作为其大小

                        3)结构体总长度必须为所有对齐参数的整数倍

编译器在默认情况下按照4字节对齐

   3、#pragma在不同的编译器间是不可移植的

          (1)预处理器将忽略它不认识#pragma指令

          (2)不同的编译器可能以不同的方式解释同一条#pragma指令

二、代码测试
1、#pragma message:自定义编译信息输出到终端(一般和#if配合使用,用在控制版本号)

#include <stdio.h>
#define ANDROID20
 
#if defined ANDROID20
   #pragma message "Compile Android SDK 2.0..."
   #define VERSION "Android 2.0"
#elif defined ANDROID30
   #pragma message "Compile Android SDK 3.0..."
   #define VERSION "Android 3.0"
#elif defined ANDROID40
   #pragma message "Compile Android SDK 4.0..."
   #define VERSION “Android 4.0”
#else
   #error Compile Version is not provided!
#endif
 
int main()
{
    printf("%s\n",VERSION);
    return 0;
}

终端输出:
在这里插入图片描述

看见没,编译的时候输出#pragma message信息,运行的时候根本看不见,你们可以通过预处理命令,看下是否在预处理阶段处理了,gcc -E test.c -o test.i 然后找到test.i文件,双击用gedit打开

看见没,预处理根本没处理#pragma message指令,所以#pragma message指令是在编译器处理的

2、#pragma once:指示编译器头文件被编译一次

#include <stdio.h>
#include "test.h"
#include "test.h"
 
int main()
{
    printf("g_value = %d\n",g_value);
    return 0;
}

test.h文件内容:

#pragma once
 
int g_value = 10; 

看下终端输出:
在这里插入图片描述

3、#pragma pack:指定内存对齐

#include <stdio.h>
 
#pragma pack(2)
struct test1
{                 //对齐参数  偏移地址   大小  
    char c1;      //1          0       1
    char c2;      //1          1       1
    short s;      //2          2       2
    int i;        //2          4       4
};
#pragma pack()
 
#pragma pack(4)
struct test2
{                 //对齐参数  偏移地址   大小
    char c1;      //1          0       1
    short s;      //2          2       2
    char c2;      //1          4       1
    int i;        //4          8       4
};
#pragma pack()
 
#pragma pack(8)
struct S1
{                 //对齐参数  偏移地址   大小 
    short s;      //2          0       2
    long l;       //4          4       4
};
struct S2
{                 //对齐参数  偏移地址   大小    
    char c;       //1          0       1
    struct S1 d;  //4          4       8        
    double e;//理论//8         16       8    这里实际是错误的,gcc不支持8字节对齐,所以这里默认是4字节对齐           //实际//4         12       8    所以整个占用内存是20字节 
};
#pragma pack()
 
int main()
{
    printf("sizeof(struct test1) = %d\n",sizeof(struct test1));  //输出8 
    printf("sizeof(struct test2) = %d\n",sizeof(struct test2));  //输出12
    
    printf("sizeof(struct S1) = %d\n",sizeof(struct S1));        //输出8
    printf("sizeof(struct S2) = %d\n",sizeof(struct S2));        //理论计算应该输出24  但是实际打印输出是20   这是因为gcc不支持8字节内存对齐,所以这里是默认4字节对齐,
    return 0;
}

看下终端打印输出:

程序里面都有注释,所以不讲了

参考资料《狄泰软件C语言进阶》

回到目录

13、【c++】size_t和size_type的区别

为了使自己的程序有很好的移植性,c++程序员应该尽量使用size_t和size_type而不是int, unsigned

  1. size_t是全局定义的类型;size_type是STL类中定义的类型属性,用以保存任意string和vector类对象的长度

  2. string::size_type 制类型一般就是unsigned int, 但是不同机器环境长度可能不同 win32 和win64上长度差别;size_type一般也是unsigned int

  3. 使用的时候可以参考:
    string::size_type a =123;
    vectorsize_type b=234;
    size_t b=456;

  4. size_t 使用的时候头文件需要 ;size_type 使用的时候需要或者

  5. sizeof(string::size_type)
    sizeof(vector::size_type)
    sizeof(vector::size_type)
    sizeof(size_t)
    上述长度均相等,长度为win32:4 win64:8

  6. 二者联系:在用下标访问元素时,vector使用vector::size_type作为下标类型,而数组下标的正确类型则是size_t

回到目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值