instance对象中的__dict__

Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式。

# 首先寻找'f'对应的descriptor(descriptor在之后会细致剖析)
# 注意:hasattr会在<class A>的mro列表中寻找符号'f'
if hasattr(A, 'f'):
descriptor = A.f
type = descriptor.__class__
if hasattr(type, '__get__') and (hasattr(type, '__set__') or 'f' not in a.__dict__):
return type.__get__(descriptor, a, A) # 通过descriptor访问失败,在instance对象自身__dict__中寻找属性
if 'f' in a.__dict__:
return a.__dict__['f'] # instance对象的__dict__中找不到属性,返回a的基类列表中某个基类里定义的函数
# 注意:这里的descriptor实际上指向了一个普通的函数
if descriptor:
return descriptor.__get__(descriptor, a, A)

  

在前一章中,我们看到从<class A>创建<instance a>时,Python虚拟机仅为a申请了16个字节的内存,并没有额外创建PyDictObject对象的动作。不过在<instance a>中,24个字节的前8个字节是PyObject,后8个字节是为两个PyObject *申请的,难道谜底就在这多出的两个PyObject *?

在创建<class A>时,我们曾说到,Python虚拟机设置了一个名为tp_dictoffset的域,从名字上判断,这个可能就是instance对象中__dict__的偏移位置。下图1-1展示了我们的猜想:

图1-1   猜想中的a.__dict__

图1-1中,虚线画的dict对象就是我们期望中的a.__dict__。这个猜想可以在PyObject_GenericGetAttr中与上述的伪代码得到证实:

object.c

PyObject * PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
PyTypeObject *tp = obj->ob_type;
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr;
……
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
PyObject *dict;
//处理变长对象
if (dictoffset < 0) {
Py_ssize_t tsize;
size_t size; tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < 0)
tsize = -tsize;
size = _PyObject_VAR_SIZE(tp, tsize); dictoffset += (long)size;
assert(dictoffset > 0);
assert(dictoffset % SIZEOF_VOID_P == 0);
}
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
……
}
}
……
}

  

如果dictoffset小于0,意味着A是继承自str这样的变长对象,Python虚拟机会对dictoffset进行一些处理,最终仍然会使dictoffset指向a的内存额外申请的位置。而PyObject_GenericGetAttr正是根据这个dictoffset获得一个dict对象。更进一步,查看函数g中有设置self(即<instance a>)中设置的a属性,这个instance对象的属性设置动作也会访问a.__dict__,而且这个动作最终调用的PyObject_GenericSetAttr也是a.__dict__最初被创建的地方:

object.c

int PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
PyTypeObject *tp = obj->ob_type;
PyObject *descr;
……
dictptr = _PyObject_GetDictPtr(obj);
if (dictptr != NULL) {
PyObject *dict = *dictptr;
if (dict == NULL && value != NULL) {
dict = PyDict_New();
if (dict == NULL)
goto done;
*dictptr = dict;
}
……
}
……
}

  

其中_PyObject_GetDictPtr的代码就是PyObject_GenericGetAttr中根据dictoffset获得dict对象的那段代码

再论descriptor

在上面的伪代码中出现了“descriptor”,这个命名其实是有意为之,目的是唤起前面我们在Python虚拟机类机制之填充tp_dict(二)这一章中所描述过的descriptor。前面我们看到,在PyType_Ready中,Python虚拟机会填充tp_dict,其中与操作名对应的是一个个descriptor,那时我们看到的是descriptor这个概念在Python内部是如何实现的。现在,我们将要剖析的是descriptor在Python的类机制究竟会起到怎样的作用

在Python虚拟机对class对象或instance对象进行属性访问时,descriptor将对属性访问的行为产生重大的影响,一般而言,对于一个Python中的对象obj,如果obj.__class__对应的class对象中存在__get__、__set__和__delete__三种操作,那么obj就可以称为Python的一个descriptor。在slotdefs中,我们会看到__get__、__set__、__delete__对应的操作:

typeobject.c

static slotdef slotdefs[] = {
……
TPSLOT("__get__", tp_descr_get, slot_tp_descr_get, wrap_descr_get,
"descr.__get__(obj[, type]) -> value"),
TPSLOT("__set__", tp_descr_set, slot_tp_descr_set, wrap_descr_set,
"descr.__set__(obj, value)"),
TPSLOT("__delete__", tp_descr_set, slot_tp_descr_set,
wrap_descr_delete, "descr.__delete__(obj)"),
……
}

  

在前面几章我们看到了PyWrapperDescrObject、PyMethodDescrObject等对象,它们对应的class对象中分别为tp_descr_get设置了wrapperdescr_get、method_get等函数,所以,它们是descriptor

如果细分,那么descriptor还可分为如下两种:

  • data descriptor:type中定义了__get__和__set__的descriptor
  • non data descriptor:type中只定义了__get__的descriptor

在Python虚拟机访问instance对象的属性时,descriptor的一个作用是影响Python虚拟机对属性的选择。从PyObject_GenericGetAttr的伪代码可以看出,Python虚拟机会在instance对象自身的__dict__中寻找属性,也会在instance对象对应的class的mro列表中寻找属性,我们将前一种属性称为instance属性,而后一种称为class属性

虽然PyObject_GenericGetAttr里对属性进行选择的算法比较复杂,但是从最终的效果上,我们可以总结处如下的两条规则:

  • Python虚拟机按照instance属性、class属性的顺序选择属性,即instance属性优先于class属性
  • 如果在class属性中发现同名的data descriptor,那么该descriptor会优先于instance属性被Python虚拟机选择

这两条规则在对属性进行设置时仍然会被严格遵守,换句话说,如果执行"a.value = 1",就算在A中发现一个名为"value"的no data descriptor,那么还是会设置a.__dict__['value'] = 1,而不会设置A中已有的属性

当最终获得的属性是一个descriptor,最神奇的事发生了,Python虚拟机不是简单的返回descriptor,而是如伪代码所示的那样,调用descriptor.__get__,将调用的结果返回,在下面的代码示例中,展示了descriptor对属性访问行为的影响:

descriptor改变返回值

>>> class A(list):
... def __get__(self, instance, owner):
... return "A.__get__"
...
>>> class B(object):
... value = A()
...
>>> b = B()
>>> b.value
'A.__get__'
>>> s = b.value
>>> type(s)
<class 'str'>

  

instance属性优先于non data descriptor

>>> class A(list):
... def __get__(self, instance, owner):
... return "A.__get__"
...
>>> class B(object):
... value = A()
...
>>> b = B()
>>> b.value = 1
>>> b.__dict__["value"]
1
>>> b.__class__.__dict__["value"]
[]

  

data descriptor优先于instance属性

>>> class A(list):
... def __get__(self, instance, owner):
... return "A.__get__"
... def __set__(self, instance, value):
... print("A.__set__")
... self.append(value)
...
>>> class B(object):
... value = A()
...
>>> b = B()
>>> b.value = 1
A.__set__
>>> b.__dict__["value"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'value'
>>> b.__class__.__dict__["value"]
[1]

  

前面我们说,当访问的属性最终对应的是一个descriptor时,会调用其__get__方法,并将__get__的结果作为返回。其实这个说法不是完全正确的,仔细对比type_getattro和PyObject_GenericGetAttr的代码,我们会发现它们在对待descriptor上存在差异。在PyObject_GenericGetAttr中,如果查询到的descriptor存在于class对象的tp_dict中,会调用其__get__方法;若它存在于instance对象的tp_dict中,则不会调用其__get__方法

>>> class A(object):
... def __get__(self, instance, owner):
... return "Python"
...
>>> class B(object):
... desc_in_class = A()
...
>>> B.desc_in_class
'Python'
>>> b = B()
>>> b.desc_in_class
'Python'
>>> b.desc_in_class = A()
>>> b.desc_in_class
<__main__.A object at 0x000000FBDD76C908>

  

到这里,我们已经看到,descriptor对属性访问的影响主要在两个方面:其一是对访问顺序的影响,其二是对访问结果的影响,第二种影响正是类的成员函数调用的关键

函数变身

demo1.py

class A(object):
name = "Python" def __init__(self):
print("A::__init__") def f(self):
print("A::f") def g(self, aValue):
self.value = aValue
print(self.value) a = A()
a.f()
a.g(10)

  

在前面讨论创建class A对象时,我们看到A.__dict__中保存了一个与符号"f"对应的PyFunctionObject对象,所以在伪代码中的descriptor对应的就是一个PyFunctionObject对象。先抛开伪代码中确定最终返回值的过程不说,我们从另一个角度来看一看,假设PyFunctionObject作为LOAD_ATTR的最终结果,在LOAD_ATTR指令代码的最后被SET_TOP压入到运行时栈,那会有什么后果呢?

在A的成员函数f的def语句中,我们看到一个self参数,self在Python中是不是一个有效的参数呢?还是它仅仅是语法意义上的占位符?这一点可以从g中看到答案,在函数g中有再这样的语句:self.value = aValue。这条语句毫无疑问地揭示了self是一个货真价实的参数,所以也表明了函数f也是一个带参函数。现在,问题来了,根据我们之前对函数机制的分析,Python通常会将参数事先压入运行时栈中,但是demo1.py中的a.f语句编译后的指令序列中可以看到,Python在获得a.f对应的对象后,没有进行任何普通函数调用时将参数压入栈的动作,而是直接执行了CALL_FUNCTION指令

a.f()调用指令

16       31 LOAD_NAME                2 (a)
34 LOAD_ATTR 3 (f)
37 CALL_FUNCTION 0
40 POP_TOP

  

这里没有任何像参数的东西在栈中,栈中只有一个可能是a.f的PyFunctionObject对象,那么这个遗失的self参数究竟在什么地方?

既然栈中没有参数,而栈中唯一的PyFunctionObject对象又需要参数,那么说明,我们之前的推理可能是错误的,所以,栈中的对象只能是另一种我们尚未了结的对象,由于是通过访问属性"f"得到的这个对象,所以一个合理的假设是:在这个对象中,还包含函数f的参数:self

在之前介绍函数机制的时候,我们似乎忘记介绍一个对象PyFunction_Type,这是PyFunctionObject对象对应的class对象,观察PyFunction_Type对象,我们会发现与__get__对应的tp_descr_get被设置为&func_descr_get,这意味着这里的A.f实际上是一个descriptor。由于PyFunc_Type中并没有设置func_descr_set,所以A.f是一个non data descriptor。此外,由于在a.__dict__中没有f符号的存在,所以根据伪代码中的算法,a.f的的返回值将被descriptor改变,其结果将是A.f.__get__,也就是func_descr_get(A.f, a, A)

funcobject.c

PyTypeObject PyFunction_Type = {
……
func_descr_get, /* tp_descr_get */
……
}; ……
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
if (obj == Py_None)
obj = NULL;
return PyMethod_New(func, obj, type);
}

  

func_descr_get将A.f对应的PyFunctionObject进行了一番包装,通过PyMethod_New在PyFunctionObject的基础上创建了一个新的对象,于是,我们再进入到PyMethod_New

funcobject.c

PyObject * PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
register PyMethodObject *im;
……
im = free_list;
if (im != NULL) {
//使用缓冲池
free_list = (PyMethodObject *)(im->im_self);
PyObject_INIT(im, &PyMethod_Type);
}
else {
//不使用缓冲池,直接创建PyMethodObject对象
im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
if (im == NULL)
return NULL;
}
im->im_weakreflist = NULL;
Py_INCREF(func);
im->im_func = func;
Py_XINCREF(self);
//这里就是self对象
im->im_self = self;
Py_XINCREF(klass);
im->im_class = klass;
_PyObject_GC_TRACK(im);
return (PyObject *)im;
}

  

这里我们可以知道,原先运行时栈中已经不再是PyFunctionObject对象,而是PyMethodObject对象。看到free_list这样熟悉的字眼,我们可以立即判断出,在PyMethodObject的实现和管理中,Python采用了缓冲池的技术,现在来看一看这个PyMethodObject

typedef struct {
PyObject_HEAD
PyObject *im_func; //可调用的PyFunctionObject对象
PyObject *im_self; //用于成员函数调用的self参数,instance对象
PyObject *im_class; //class对象
PyObject *im_weakreflist;
} PyMethodObject;

  

在PyMethod_New中,分别将im_func、im_self、im_class设置了不同的值,结合a.f,分别对应符号"f"所对应的PyFunctionObject对象,符号"a"对应的instance对象,以及<class A>对象

在Python中,将PyFunctionObject对象和一个instance对象通过PyMethodObject对象结合在一起的过程就称为成员函数的绑定。下面的代码清晰地展示了在访问属性时,发生函数绑定的结果:

>>> class A(object):
... def f(self):
... pass
...
>>> a = A()
>>> a.__class__.__dict__["f"]
<function A.f at 0x000000FBDD74E620>
>>> a.f
<bound method A.f of <__main__.A object at 0x000000FBDD76CE80>>

  

无参函数的调用

在LOAD_ATTR指令之后,指令"37   CALL_FUNCTION   0"开始了函数调用的动作,之前我们研究过对于PyFunctionObject对象的调用,而对于PyMethodObject对象,情况则有些不同,如下:

ceval.c

static PyObject * call_function(PyObject ***pp_stack, int oparg)
{
int na = oparg & 0xff;
int nk = (oparg >> 8) & 0xff;
int n = na + 2 * nk;
PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;
PyObject *x, *w; ……
if (PyCFunction_Check(func) && nk == 0)
{
……
}
else
{ //[1]:从PyMethodObject对象中抽取PyFunctionObject对象和self参数
if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL)
{
PyObject *self = PyMethod_GET_SELF(func);
func = PyMethod_GET_FUNCTION(func);
//[2]:self参数入栈,调整参数信息变量
*pfunc = self;
na++;
n++;
}
if (PyFunction_Check(func))
x = fast_function(func, pp_stack, n, na, nk);
else
x = do_call(func, pp_stack, na, nk);
……
}
……
return x;
}

  

调用成员函数f时,显示传入的参数个数为0,也就是说,调用f时,Python虚拟机没有进行参数入栈的动作。而f显然至少需要一个实例对象的参数,而正是在call_function中,Python虚拟机为PyMethodObject进行了一些参数处理的动作

Python虚拟机执行a.f()时,在call_function中,代码[1]处的判断将会成立,其中PyMethod_GET_SELF被定义为:

classobject.h

#define PyMethod_GET_SELF(meth) \
(((PyMethodObject *)meth) -> im_self)

  

在call_function中,func变量指向一个PyMethodObject对象,在上述代码[1]处成立后,在if分支中又会将PyMethodObject对象中的PyFunctionObject对象和instance对象分别提取出来,在if分支中有一处最重要的代码,即[2]处,pfunc指向的位置正是运行时栈中存放PyMethodObject对象的位置,那么这个本来属于PyMethodObject对象的地方改为存放instance对象究竟有什么作用呢?在这里,Python虚拟机以另一种方式完成了函数参数入栈的动作,本来属于PyMethodObject对象的内存空间被用作了函数f的self参数的容身之处,图1-1展示了运行call_function时运行时栈的变化情况:

图1-1   设置self参数

a是设置pfunc之前的运行时栈,b表示设置了pfunc之后的运行时栈。在call_function中,接着还会通过PyMethod_GET_FUNCTION将PyMethodObject对象中的PyFunctionObject对象取出,随后在[2]处,Python虚拟机完成了self参数的入栈,同时还调整了维护着参数信息的na和n,调整后的结果意味着函数会获得一个位置参数,看一看class A中的f的def语句,self正是一个位置参数

由于func在if分支之后指向了PyFunctionObject对象,所以接下来Python执行引擎将进入fast_function。到了这里,剩下的动作就和我们之前所分析的带参函数的调用一致。实际上a.f的调用是指上就是一个带一个位置参数的一般函数调用,而在fast_function,作为self参数的<instance a>被Python虚拟机压入到了运行时栈中,由于a.f仅仅是一个带位置参数的函数,所以Python执行引擎将进入快速通道,在快速通道中,运行时栈中的这个instance对象会被拷贝到新的PyFrameObject对象的f_localsplus中

ceval.c

static PyObject * fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
PyObject *globals = PyFunction_GET_GLOBALS(func);
PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
PyObject **d = NULL;
int nd = 0; PCALL(PCALL_FUNCTION);
PCALL(PCALL_FAST_FUNCTION);
if (argdefs == NULL && co->co_argcount == n && nk == 0 &&
co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
{
//创建新的PyFrameObject对象f
PyFrameObject *f;
f = PyFrame_New(tstate, co, globals, NULL);
if (f == NULL)
return NULL; fastlocals = f->f_localsplus;
//[1]:获得栈顶指针
stack = (*pp_stack) - n; for (i = 0; i < n; i++)
{
//[2]:
fastlocals[i] = *stack++;
}
……
}
……
}

  

在调用fast_function时,参数的数量n已经由执行CALL_FUNCTION时的0变为了1,所以代码[1]处的stack指向的位置就和图1-1中pfunc指向的位置是一致的了,在代码的[2]处将<instance a>作为参数拷贝到函数的参数区fastlocals中,必须将它放置到栈顶,也就是以前PyMethodObject对象所在的位置上,也也就是前面call_function那个赋值操作的原因

带参函数的调用

Python虚拟机对类中带参的成员函数的调用,其原理和流程与无参函数的调用是一致的,我们来看看a.g(10)的字节码序列:

17       41 LOAD_NAME                2 (a)
44 LOAD_ATTR 4 (g)
47 LOAD_CONST 2 (10)
50 CALL_FUNCTION 1
53 POP_TOP

  

可以看到,和调用成员函数f的指令序列几乎完全一致,只是多了一个"47   LOAD_CONST   2 (10)"。对于这个指令我们不会陌生,在分析函数机制的时候,我们看到它是用来将函数所需的参数压入到运行时栈中。对于g,真正有趣的地方在于考察函数的实现代码,从而可以看到那个作为self参数的instance对象的使用:

>>> dis.dis(A.g)
11 0 LOAD_FAST 1 (aValue)
3 LOAD_FAST 0 (self)
6 STORE_ATTR 0 (value) 12 9 LOAD_FAST 0 (self)
12 LOAD_ATTR 0 (value)
15 PRINT_ITEM
16 PRINT_NEWLINE

  

显然,其中的LOAD_FAST、LOAD_ATTR、STORE_ATTR这些字节码指令都涉及到了作为self参数的instance对象,有兴趣的同学可以分析一下STORE_ATTR的代码,可以发现其中也有类似于LOAD_ATTR中PyObject_GenericGetAttr的属性访问算法

其实到了这里,我们可以在更高的层次俯视一下Python的运行模型,最核心的模型其实非常简单,可以简化为两条规则:

  • 在某个名字空间中寻找符号对应的对象
  • 对从名字空间中得到的对象进行某些操作

抛开面向对象花里胡哨的外表,其实我们会发现,class对象其实就是一个名字空间,instance对象也是一个名字空间,不过这些名字空间通过一些特殊的规则关联在一起,使得符号的搜索过程变得复杂,从而实现了面向对象这种编程模式

Python虚拟机类机制之instance对象(六)的更多相关文章

  1. Python虚拟机类机制之从class对象到instance对象(五)

    从class对象到instance对象 现在,我们来看看如何通过class对象,创建instance对象 demo1.py class A(object): name = "Python&q ...

  2. Python虚拟机类机制之绑定方法和非绑定方法(七)

    Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...

  3. Python虚拟机类机制之descriptor(三)

    从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...

  4. Python虚拟机类机制之填充tp_dict(二)

    填充tp_dict 在Python虚拟机类机制之对象模型(一)这一章中,我们介绍了Python的内置类型type如果要完成到class对象的转变,有一个重要的步骤就是填充tp_dict对象,这是一个极 ...

  5. Python虚拟机类机制之自定义class(四)

    用户自定义class 在本章中,我们将研究对用户自定义class的剖析,在demo1.py中,我们将研究单个class的实现,所以在这里并没有关于继承及多态的讨论.然而在demo1.py中,我们看到了 ...

  6. Python虚拟机类机制之对象模型(一)

    Python对象模型 在Python2.2之前,Python中存在着一个巨大的裂缝,就是Python的内置类type,比如:int和dict,这些内置类与程序员在Python中自定义的类并不是同一级别 ...

  7. Python虚拟机函数机制之扩展位置参数和扩展键参数(六)

    扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...

  8. Python虚拟机函数机制之位置参数的默认值(五)

    位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...

  9. python基础——类名称空间与对象(实例)名称空间

    python基础--类名称空间与对象(实例)名称空间 1 类名称空间 创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性 而类的良好总属性:数据属性和函数属性 其中类 ...

随机推荐

  1. SpringMVC 返回自定义属性名

    SpringMVC 返回的属性名默认是小写驼峰形式的实体对象中的属性名,如 userID 属性名它会返回 userId. 如果接口方式之前已经定下来,这样前端按原来的方式取数据会读取失败的,那有没有方 ...

  2. 使用Python开发环境Wing IDE设立项目注意事项

    使用Wing IDE的第一步是建立一个项目文件,这样Wing IDE就可以找到并分析源代码,存储工作. Wing IDE会自动以默认的项目进行启动.在本教程中用户也可以使用这个默认项目进行示例操作.如 ...

  3. IIS 处理高并发

    1. 调整IIS 7应用程序池队列长度 由原来的默认1000改为65535. IIS Manager > ApplicationPools > Advanced Settings Queu ...

  4. uvm_regex——DPI在UVM中的实现(三)

    UVM的正则表达是在uvm_regex.cc 和uvm_regex.svh 中实现的,uvm_regex.svh实现UVM的正则表达式的源代码如下: `ifndef UVM_REGEX_NO_DPI ...

  5. Elasticsearch-2.3 (OpenLogic CentOS 7.2)

    平台: CentOS 类型: 虚拟机镜像 软件包: elasticsearch-2.3 application server basic software big data elasticsearch ...

  6. 编程之美2015 资格赛 hihocoder 题目2: 回文字符序列

    思路:暴力搜,用BFS的方式,生成每一种可能,再对每一种可能进行判断是否回文,进行统计.严重超时!计算一个25个字符的,大概要20多秒! #include <iostream> #incl ...

  7. linux 命令——39 grep (转)

    Linux系统中grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹 配的行打印出来.grep全称是Global Regular Expression Print,表示全局正则表达 ...

  8. linux 命令——35 ln(转)

    ln 是linux中又一个非常重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接.当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要 ...

  9. [VC]strcpy和strncoy的区别

    第一种情况:char* p="how are you ?";char name[20]="ABCDEFGHIJKLMNOPQRS"; strcpy(name,p ...

  10. angular4路由设置笔记

    场景说明:angular4开发的一个后台项目 一.可以使用angular-cli创建一个带路由的项目,ng new 项目名称 --routing 会多创建一个app-routing.module.ts ...