《C++程序设计语言》笔记之一

本文详细介绍了C++中的基本类型,包括布尔类型、枚举类型及其转换规则,同时深入探讨了对象的声明、初始化及生命周期管理。此外,还讨论了如何通过构造函数和析构函数控制对象的创建与销毁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 基本类型:


bool类型:
按照定义true具有值1,false具有值0。于此对应,整数可以隐式地转换为bool值,非零的整数转换为true,而0转换为false。
在算术和逻辑表达式里面,bool都将被转为int,在转换为int后进行相应的计算。

bool b = 8; // bool(8)是true,所以b赋值为true
int i = true; // int(true)是1,因此i变为1

bool a = true;
bool b = true;
bool x = a + b;// a + b 为2,因此x为 true
bool y = a | b;// a | b 为1,所以y为 true

enum类型:
enum keyword{ ASM, AUTO, BREAK};

枚举类型可以用整型的constant-expression进行初始化。如果某个枚举中所有的枚举符的值均非负,该枚举的表示范围就是[0:2^k - 1].
如果存在负的枚举值,该枚举的取值范围是[-2^k : 2^k - 1]。

一个整数值显示地转化到一个枚举值。除非这种转换的结果位于枚举值的范围之内,否则就是无定义的。
enum flag{x=1, y=2, z=4, e=8};
flag f1 = 5; // 类型错误,5不是flag类型
flag f2 = flag(5);// 可以:flag(5)是flag类型,且在flag范围之内
flag f3 = flag(z|e);// 可以flag(12)是flag类型,且在flag的范围之内
flag f4 = flag(99);// 无定义,99不在flag的范围之内

由于大部分的整数在特定的枚举类型下都没有对应的表示,因此不允许隐式地从整数转化为enum

sizeof可以获得一个枚举类型变量的内存大小,其最大不会大于sizeof(int).在sizeof(int) == 4的机器上,sizeof(el)可能是1,也可能是4
但是绝对不会是8.


声明的结构:
一个声明由四部分组成:一个可选的"描述符",一个基础类型,一个声明符,还有一个可选的初始式。

例如 char * kings[] = {"Antigonus", "sele", "Ptolemy"};
基础类型是 char,声明符 *kings[] ,而初始式是 ={};

声明符由一个名字和可选的若干运算符组成,常用声明运算符为:
* 指针前缀
*const 常量指针 前缀
& 引用前缀
[] 数组后缀
() 函数后缀

声明多个名字:

注意运算符是声明符的一部分,因此它仅仅作用于一个单独名字。

例如 int *p, y; // int *p; int y,而并非 int *y;

变量的初始化:

C++不提供所有变量的初始化。只有全局的,名字空间的,局部静态的对象或变量自动初始化为适当类型的0.

在一个函数里声明的对象在其定义被遇到之时建立,在它的名字离开作用域的时候被销毁,这种对象被称作自动对象。
在全局和名字空间作用域里的对象,以及函数和类里面声明为static的对象只建立一次,他们一直存在到程序结束。这种对象为静态对象。
数组成员,非静态结构和类的成员由他们所属的对象决定。
new和delete可以建立 程序员控制的对象


常量:

用户定义常量,使用const。常量在整个程序中是不再变的值,因此定义时必须进行初始化。

常量的初始式是常量表达式,如果确实是这样,那么这个常量可以在编译时进行求值,也即编译器知道了某const的所有使用,
编译器可以不为const分配空间。
const int c1 = 1;
const int c2 = 2;
const int c3 = my_f(3);// 编译时不知道c3的值
extern const int c4;// 编译时不知道c4的值
const int * p = &c2;// 需要为c2分配空间

编译器知道c1的值,因此这些值可以用到常量表达式里面,放到符号表中,不分配空间。
虽然c2也可以用到常量表达式,但是由于下面会对其取地址,因此需要对其分配空间
而c3和c4,由于无法知道其值,必须要进行空间分配。

指针和常量:
char * const cp;
char const * pc;
const char * pc;

静态变量:

如果一个局部变量被声明为static,那么将只有唯一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量
这个变量将只在执行线程第一次到达它的定义时初始化。后面再次到达它的定义经不被重新定义和初始化。

引用:

一个引用就是某个对象的另一个名字,引用的主要用途是为了描述函数的参数和返回值,特别为了运算符重载。记法X&表示到X的引用

为了确保一个引用总能是某个东西的名字,必须对引用作初始化。对于一个引用的初始化 与对它赋值完全不同的事情。除了外表形式之外,
实际上根本就没有能操作引用的运算符操作。
int ii = 0;
int & rr = ii;
rr++;
int *pp = &rr;
rr++ 并不是对rr本身的增量,而是对其引用的值的增量。要取得rr所引用对象的地址,可以写&rr。

引用的一种最明显的实现方式是作为一个(常量)指针,在每次使用时都自动地做间接访问。将引用想象成这种样子不会有问题,
但是要记住的是一个引用并不是一个对象,不能像指针那样去操作。

其中 const T&的初始式不必是一个左值,甚至可以不是类型T的:
1. 首先,如果需要应用到T的隐式类型转换
2. 而后将结果存入一个类型T的临时变量
3. 将临时变量用作初始式的值
考虑
double & dr = 1;// 不可以
const double& cdr = 1;// OK的
对后面一个的解释:
double temp = double(1);// 首先建立一个具有正确值的临时变量
const double & cdr = temp;// 而后用这个临时变量作为cdr的初始式
那么这个保存初始式的临时变量将一直存在,直到这个引用的作用域结束。

extern "C" 的用法:

由于C和C++的编译器的不同,造成两个之间函数的调用存储在问题。

如果需要在C++中调用C的函数,对于使用C语言写的头文件或者 函数需要使用extern "C"进行说明,以便在编译时,将这些头文件或
函数调用C编译器进行编译,编译出可以在C++中进行调用的函数。
例如:
extern "C"{
char * strcpy( char *, const char *);
int strcmp( const char *, const char *);
int strlen(const char *);
}

extern "C"{
#include <string.h>
}

如果在C语言中需要调用C++的函数,那么在C++文件中,需要将这些被C调用的函数进行 extern "C" 的说明。
// C++ code:
extern "C" void f(int);
void f(int i)
{
// ...
}
 
然后,你可以这样使用 f():
/* C code: */
void f(int);
void cc(int i)
{
f(i);
  /* ... */
}

对于C++的成员函数,需要对其进行进一步封装,专门写一个接口函数,在其中对成员函数进行调用,对该接口函数使用extern "C"

进行说明。

2. 类

复制函数,以及赋值函数:
默认情况下,这两个函数都是进行按成员复制,也即C语言中的按位复制。如果想要修改默认的复制,根据对象进行特别赋值。
需要多赋值构造函数进行重载 operator=();

常量成员函数:
在啊很难数声明的参数表后面出现的 const,指明了这些函数不会修改Date的状态。

class Date{
int d, m, y;
public:
int day() const {return d;};
int month() const{ return m;};
int year() const;
};

如果尝试在const的成员函数中修改成员,将出现编译错误。例如:
inline int Date::year() const
{
return y++;
}

const和非const对象都可以调用const成员函数,而非const成员函数只能对非const对象调用

自引用:


对于状态更新函数add_year(),add_month()以及add_day()都被定义为不返回值的函数,多余这样的一组相关的更新函数,
可以让他们返回一个到被更新对象的引用,以使对于对象的操作可以串联起来。
例如希望写为:
d.add_year(1).add_month(1).add_year(1);

因此需要每一个函数返回一个到Date的引用:
class Date{
// ....
Date& add_year(int n);
Date& add_month(int n);
Date& add_day(int n);
};
每一个函数(非静态)都知道它是被那个对象调用的,可以显示地引用该对象。例如:
Date& Date::add_year(int n)
{
if(d== 29 && m==2 && !leapyear(y+n))
{
d = 1;
m = 3;
}
y += n;
return *this;
}

在类X的const成员函数里,this的类型是 const X*,以防止对于这个对象本身的修改。
然而this并不是一个常规的变量,不能取得this的地址或给this赋值。

const成员函数修改成员变量:
一般情况下,如果要在const成员中修改成员变量。可以通过将里面的this指针去掉const,从而修改对应对象的成员。

例如:
string Date::string_rep() const
{
if(cache_valid == false)
{
Date* th = const_cast<Date *>(this);
th->compute_cache_value();
th->cache_valid = true;
}
return cache;
}

在一种情况是可以通过mutable关键字实现:
显示类型转化"强制去掉const",以及由它引起的依赖于实现的行为还是可以避免的,只要将变量声明为mutable即可。
class Date{
mutable bool cache_valid;
mutable string cache;
void compute_cache_value() const;

public:
string string_rep() const;
};

关键字mutable特别说明了这个成员需要以一种能允许更新的方式存储,即时它是某个const对象的成员。

协助函数:

有这样一种情况,一个类有一批与它相关的函数,但是它们又未必要定义在类里面,因为他们不需要直接访问有关的表示,
例如:
int diff(Date a, Date b);
bool leapyear( int y);
Date next_weedday(Date d);
在类中定义这些函数使得类的界面复杂化,也增加了改变表示时需要检查的函数的个数。

按照传统的组织方式,他们声明将直接与Date的声明放在一个文件里,那些需要Date的用户在包含了定义它的界面文件时,也就使
这些函数可用。

另外一种替代方式可以将这种关联明确化,将类和它的协助函数包含在同一个名字空间里。
namespace Chrono}{
class Date{};
int diff(Date a, Date b);
bool leapyear( int y);
Date next_weedday(Date d);
}

构造函数和析构函数:

建立对象的各种方式:
1. 一个命名的自动对象,当程序执行每次遇到它的声明时建立,每次程序离开它所出现的块时销毁。
对象的复制:
void h()
{
Table t1;
Table t2 = t1;
Table t3;
t3 = t2;
}

这里Table的默认构造函数为t1和t3各调用一次,一共两次,然而Table的析构函数则被调用了3次,对t1,t2和t3各一次。
如果Table中包含了一个指针,指向一个从自由空间分配的内存。如果构造函数中有对该内存的分配,那么t2 和t3的赋值将
覆盖掉他们自己的指针,从而指向t1 的自由空间,那么t2 和t3原来所指向的自由空间将发生内存泄露,同时在三个对象析构
时将发生致命的错误,对t1所指向的自由空间进行多次释放。

因此在有指针的成员时,要给出 复制构造函数 和 赋值运算符重载。
class Table{
Table(const Table&);
Table& operator=(const Table&);
};

2. 一个自由存储对象,通过new运算符建立,通过delete运算符销毁

3. 一个非静态成员对象,作为另一个类对象的成员,在它作为成员的那个对象建立或销毁时,它也随之被建立或销毁
要使用成员初始化列表
Club::Club(const string& n, Date fd): name(n), founded(fd)
{
}

成员对象的构造函数将在容器类本身的构造函数体执行之前首先被执行。并且是按照对象声明的顺序来执行,而非
初始化列表中的顺序来执行。

为何要进行初始化:
主要是针对初始化与赋值不同的情况,对于那些没有默认构造函数的类的成员对象,对于那些const成员,引用成员而言,
对成员对象的初始式都是必不可少的。

例如:
class X{
const int i;
Club c;
Club& pc;

X(int ii, const string& n, Date d, Club&c): i(ii), c(n,d), pc(c)
{
}
};

注:如果存在着某个非静态成员是一个引用,const或者属于没有复制赋值的用户定义类型,那么也无法生成默认的复制赋值。
默认的复制构造函数会导致原对象和复制出来的对象的引用成员引用着同一个对象,如果被引用的对象后来被删除,那么会造成问题。

4. 一个数组元素,在它作为元素的那个数组被建立或销毁的时候建立或销毁

5. 一个局部静态对象,在程序执行中第一次遇到它的生命是建立一次,在程序终止时销毁一次。

6. 一个全局对象,名字空间的对象,类的静态对象,他们只在程序开始时建立一次,程序终止时销毁一次

7. 一个临时对象,作为表达式求值的一部分被建立,在它所出现的那个完整表达式的最后被销毁

临时对象常常作为算术表达式的结果出现:例如 x * y + z中,部分结果 x * y 必须存储在某个地方。

除非一个临时对象被约束到某个引用,或者被用于命名对象的初始化,否则临时对象总在建立它的表达式结束时销毁
所谓完整表达式就是不是其他表达式子表达式的表达式。

例如string类有一个成员C_str()返回一个C风格的以0结尾的字符数组。此外 + 定义为串接字符串。
void f(string& s1, string& st2, string& s3)
{
const char * cs = (s1 + s2).c_str();
cout<<cs;

if(strlen(cs=(s2+s3).c_str()) < 8 && cs[0] == 'a')
{
//使用cs
}
}
这样的代码是错误的。

s1 + s2的语句结束时,他们相加产生的临时对象也就销毁,cs指针指向的将是释放掉的内存,使用它很危险。


8. 一个在分配操作中由所提供的参数控制,在通过用户提供的函数获得存储里放置的对象
按照默认方式,运算符new将在自由存储中创建对象,需要在其他的地方分配对象,怎么办?
一个简单类:
class X{
public:
X(int);
// ...
};

通过提供一个带额外参数的分配函数,而后再使用new时提供对应参数的方式,我们可以将对象放在任何地方。
void* operator new(size_t, void *p){ return p;}// 显示放置运算符
void *buf = reinterpret_cast<void *>(0xF00F);
X* p2 = new(buf)X;// 在buf 构造X时调用: operator new(sizeof(X), buf)

由于这种方式,为operator new提供额外的参数的new(buf)X 的这种语法形式被称为“放置语法”

reinterpret_cast<void*> 将指针或地址转化为指定类型。

放置式 的new结构可以用于从某个特定的场地中分配存储。
class Arena{
public:
virtual void * alloc(size_t) = 0;
virtual void free(void *) = 0;
//...
};
void * operator new(size_t sz, Arena *a)
{
return a->alloc(sz);
}
可以在不同的Arena分配任意类型的对象了,例如:
extern Arena * Persistent;
extern Arena * Shared;
void g(int i)
{
X *p = new(Persistent)X(i);
X *q = new(Shared)X(i);
}

在销毁对象是,要显示调用析构函数:
void destroy( X* p, Arena* a)
{
p-> ~X();
a->free(p);
}
不仅要调用X的析构函数,同时要将指针指向的内存释放。

9. 一个union成员,它不能有构造函数和析构函数


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值