CString Management (关于CString的所有操作)

golang基础之 cgo与c语言交互 使用Cgo需要使用#cgo指令,并遵循一些技巧,如使用“unsafe.Pointer”和“C.CString”。在这个示例中,我们定义了一个字符串“str”,并使用“C.CString”将其转换为C语言字符串。然后,我们调用了C语言函数“baz”,该函数返回另一个C语言字符串,我们使用“C.GoString”将其转换为Golang字符串,并输出到控制台。然后,我们使用“unsafe.Pointer”将指针转换为C语言指针,并将其传递给C语言函数“bar”。在使用Cgo时,需要用到#cgo指令。 阅读详情

CString Management (关于CString的所有操作)
作者:FreeEIM
CStrings are a useful data type. They greatly simplify a lot of operations in MFC, making it much more convenient to do string manipulation. However, there are some special techniques to using CStrings, particularly hard for people coming from a pure-C background to learn. This essay discusses some of these techniques.

Much of what you need to do is pretty straightforward. This is not a complete tutorial on CStrings, but captures the most common basic questions.

CString concatenation
Formatting (including integer-to-CString)
Converting CStrings to integers 
Converting between char * to a CString 
     char * to CString
     CString to char * I: Casting to LPCTSTR
     CString to char * II: Using GetBuffer
     CString to char * III: Interfacing to a control
CString to BSTR  
BSTR to CString  (30-Jan-01)
VARIANT to CString   (24-Feb-01)
Loading STRINGTABLE resources  (22-Feb-01)
CStrings and temporary objects  
CString efficiency
String ConcatenationOne of the very convenient features of CString is the ability to concatenate two strings. For example if we have

CString gray("Gray");
CString cat("Cat");
CString graycat = gray + cat;
is a lot nicer than having to do something like:

char gray[] = "Gray";
char cat[] = "Cat";
char * graycat = malloc(strlen(gray) + strlen(cat) + 1);
strcpy(graycat, gray);
strcat(graycat, cat);
Formatting (including integer-to-CString)
Rather than using sprintf or wsprintf, you can do formatting for a CString by using the Format method:

CString s;
s.Format(_T("The total is %d"), total);
The advantage here is that you don't have to worry about whether or not the buffer is large enough to hold the formatted data; this is handled for you by the formatting routines.

Use of formatting is the most common way of converting from non-string data types to a CString, for example, converting an integer to a CString:

CString s;
s.Format(_T("%d"), total);
I always use the _T( ) macro because I design my programs to be at least Unicode-aware, but that's a topic for some other essay. The purpose of _T( ) is to compile a string for an 8-bit-character application as:

#define _T(x) x // non-Unicode version
whereas for a Unicode application it is defined as

#define _T(x) L##x // Unicode version
so in Unicode the effect is as if I had written

s.Format(L"%d", total);
If you ever think you might ever possibly use Unicode, start coding in a Unicode-aware fashion. For example, never, ever use sizeof( ) to get the size of a character buffer, because it will be off by a factor of 2 in a Unicode application. We cover Unicode in some detail in Win32 Programming. When I need a size, I have a macro called DIM, which is defined in a file dim.h that I include everywhere:

#define DIM(x) ( sizeof((x)) / sizeof((x)[0]) )
This is not only useful for dealing with Unicode buffers whose size is fixed at compile time, but any compile-time defined table.

class Whatever { ... };
Whatever data[] = {
   { ... },
    ...
   { ... },
};

for(int i = 0; i < DIM(data); i++) // scan the table looking for a match
Beware of those API calls that want genuine byte counts; using a character count will not work.

TCHAR data[20];
lstrcpyn(data, longstring, sizeof(data) - 1); // WRONG!
lstrcpyn(data, longstring, DIM(data) - 1); // RIGHT
WriteFile(f, data, DIM(data), &bytesWritten, NULL); // WRONG!
WriteFile(f, data, sizeof(data), &bytesWritten, NULL); // RIGHT
This is because lstrcpyn wants a character count, but WriteFile wants a byte count. Also note that this always writes out the entire contents of data. If you only want to write out the actual length of the data, you would think you might do

WriteFile(f, data, lstrlen(data), &bytesWritten, NULL); // WRONG
but that will not work in a Unicode application. Instead, you must do

WriteFile(f, data, lstrlen(data) * sizeof(TCHAR), &bytesWritten, NULL); // RIGHT
because WriteFile wants a byte count. (For those of you who might be tempted to say "but that means I'll always be multiplying by 1 for ordinary applications, and that is inefficient", you need to understand what compilers actually do. No real C or C++ compiler would actually compile a multiply instruction inline; the multiply-by-one is simply discarded by the compiler as being a silly thing to do. And if you think when you use Unicode that you'll have to pay the cost of multiplying by 2, remember that this is just a bit-shift left by 1 bit, which the compiler is also happy to do instead of the multiplication).

Using _T does not create a Unicode application. It creates a Unicode-aware application. When you compile in the default 8-bit mode, you get a "normal" 8-bit program; when you compile in Unicode mode, you get a Unicode (16-bit-character) application. Note that a CString in a Unicode application is a string that holds 16-bit characters.

Converting a CString to an integerThe simplest way to convert a CString to an integer value is to use one of the standard string-to-integer conversion routines.

While generally you will suspect that _atoi is a good choice, it is rarely the right choice. If you play to be Unicode-ready, you should call the function _ttoi, which compiles into _atoi in ANSI code and _wtoi in Unicode code. You can also consider using _tcstoul (for unsigned conversion to any radix, such as 2, 8, 10 or 16) or _tcstol (for signed conversion to any radix). For example, here are some examples:

CString hex = _T("FAB");
CString decimal = _T("4011");
ASSERT(_tcstoul(hex, 0, 16) == _ttoi(decimal));
Converting between char * and CStringThis is the most common set of questions beginners have on the CString data type. Due largely to serious C++ magic, you can largely ignore many of the problems. Things just "work right". The problems come about when you don't understand the basic mechanisms and then don't understand why something that seems obvious doesn't work.

For example, having noticed the above example you might wonder why you can't write

CString graycat = "Gray" + "Cat";
or

CString graycat("Gray" + "Cat");
In fact the compiler will complain bitterly about these attempts. Why? Because the + operator is defined as an overloaded operator on various combinations of the CString and LPCTSTR data types, but not between two LPCTSTR data types, which are underlying data types. You can't overload C++ operators on base types like int and char, or char *. What will work is

CString graycat = CString("Gray") + CString("Cat");
or even

CString graycat = CString("Gray") + "Cat";
If you study these, you will see that the + always applies to at least one CString and one LPCSTR.

char * to CStringSo you have a char *, or a string. How do you create a CString. Here are some examples:

char * p = "This is a test"
or, in Unicode-aware applications

TCHAR * p = _T("This is a test")
or

LPTSTR p = _T("This is a test");
you can write any of the following:

CString s = "This is a test";     // 8-bit only
CString s = _T("This is a test"); // Unicode-aware
CString s("This is a test");      // 8-bit only
CSTring s(_T("This is a test");   // Unicode-aware
CString s = p;
CString s(p);
Any of these readily convert the constant string or the pointer to a CString value. Note that the characters assigned are always copied into the CString so that you can do something like

TCHAR * p = _T("Gray");
CString s(p);
p = _T("Cat");
s += p;
and be sure that the resulting string is "GrayCat".

There are several other methods for CString constructors, but we will not consider most of these here; you can read about them on your own.

CString to char * I: Casting to LPCTSTRThis is a slightly harder transition to find out about, and there is lots of confusion about the "right" way to do it. There are quite a few right ways, and probably an equal number of wrong ways.

The first thing you have to understand about a CString is that it is a special C++ object which contains three values: a pointer to a buffer, a count of the valid characters in the buffer, and a buffer length. The count of the number of characters can be any size from 0 up to the maximum length of the buffer minus one (for the NUL byte). The character count and buffer length are cleverly hidden.

Unless you do some special things, you know nothing about the size of the buffer that is associated with the CString. Therefore, if you can get the address of the buffer, you cannot change its contents. You cannot shorten the contents, and you absolutely must not lengthen the contents. This leads to some at-first-glance odd workarounds.

The operator LPCTSTR (or more specifically, the operator const TCHAR *), is overloaded for CString. The definition of the operator is to return the address of the buffer. Thus, if you need a string pointer to the CString you can do something like

CString s("GrayCat");
LPCTSTR p =  s;
and it works correctly. This is because of the rules about how casting is done in C; when a cast is required, C++ rules allow the cast to be selected. For example, you could define (float) as a cast on a complex number (a pair of floats) and define it to return only the first float (called the "real part") of the complex number so you could say

Complex c(1.2f, 4.8f);
float realpart = c;
and expect to see, if the (float) operator is defined properly, that the value of realpart is now 1.2.

This works for you in all kinds of places. For example, any function that takes an LPCTSTR parameter will force this coercion, so that you can have a function (perhaps in a DLL you bought):

BOOL DoSomethingCool(LPCTSTR s);
and call it as follows

CString file("c://myfiles//coolstuff")
BOOL result = DoSomethingCool(file);
This works correctly because the DoSomethingCool function has specified that it wants an LPCTSTR and therefore the LPCTSTR operator is applied to the argument, which in MFC means that the address of the string is returned.

But what if you want to format it?

CString graycat("GrayCat");
CString s;
s.Format("Mew! I love %s", graycat);
Note that because the value appears in the variable-argument list (the list designated by "..." in the specification of the function) that there is no implicit coercion operator. What are you going to get?

Well, surprise, you actually get the string

"Mew! I love GrayCat"
because the MFC implementers carefully designed the CString data type so that an expression of type CString evaluates to the pointer to the string, so in the absence of any casting, such as in a Format or sprintf, you will still get the correct behavior. The additional data that describes a CString actually lives in the addresses below the nominal CString address.

What you can't do is modify the string. For example, you might try to do something like replace the "." by a "," (don't do it this way, you should use the National Language Support features for decimal conversions if you care about internationalization, but this makes a simple example):

CString v("1.00");  // currency amount, 2 decimal places
LPCTSTR p = v;
p[lstrlen(p) - 3] = ',';
If you try to do this, the compiler will complain that you are assigning to a constant string. This is the correct message. It would also complain if you tried

strcat(p, "each");
because strcat wants an LPTSTR as its first argument and you gave it an LPCTSTR.

Don't try to defeat these error messages. You will get yourself into trouble!

The reason is that the buffer has a count, which is inaccessible to you (it's in that hidden area that sits below the CString address), and if you change the string, you won't see the change reflected in the character count for the buffer. Furthermore, if the string happens to be just about as long as the buffer physical limit (more on this later), an attempt to extend the string will overwrite whatever is beyond the buffer, which is memory you have no right to write (right?) and you'll damage memory you don't own. Sure recipe for a dead application.

CString to char * II: Using GetBufferA special method is available for a CString if you need to modify it. This is the operation GetBuffer. What this does is return to you a pointer to the buffer which is considered writeable. If you are only going to change characters or shorten the string, you are now free to do so:

CString s(_T("File.ext"));
LPTSTR p = s.GetBuffer();
LPTSTR dot = strchr(p, '.'); // OK, should have used s.Find...
if(p != NULL)
    *p = _T('/0');
s.ReleaseBuffer();
This is the first and simplest use of GetBuffer. You don't supply an argument, so the default of 0 is used, which means "give me a pointer to the string; I promise to not extend the string". When you call ReleaseBuffer, the actual length of the string is recomputed and stored in the CString. Within the scope of a GetBuffer/ReleaseBuffer sequene, and I emphasize this: You Must Not, Ever, Use Any Method Of CString on the CString whose buffer you have! The reason for this is that the integrity of the CString object is not guaranteed until the ReleaseBuffer is called. Study the code below:

CString s(...);
LPTSTR p = s.GetBuffer();
//... lots of things happen via the pointer p
int n = s.GetLength(); // BAD!!!!! PROBABLY WILL GIVE WRONG ANSWER!!!
s.TrimRight();         // BAD!!!!! NO GUARANTEE IT WILL WORK!!!!
s.ReleaseBuffer();     // Things are now OK
int m = s.GetLength(); // This is guaranteed to be correct
s.TrimRight();         // Will work correctly
Suppose you want to actually extend the string. In this case you must know how large the string will get. This is just like declaring

char buffer[1024];
knowing that 1024 is more than enough space for anything you are going to do. The equivalent in the CString world is

LPTSTR p = s.GetBuffer(1024);
This call gives you not only a pointer to the buffer, but guarantees that the buffer will be (at least) 1024 bytes in length.

Also, note that if you have a pointer to a const string, the string value itself is stored in read-only memory; an attempt to store into it, even if you've done GetBuffer, you have a pointer to read-only memory, so an attempt to store into the string will fail with an access error. I haven't verified this for CString, but I've seen ordinary C programmers make this error frequently.

A common "bad idiom" left over from C programmers is to allocate a buffer of fixed size, do a sprintf into it, and assign it to a CString:

char buffer[256];
sprintf(buffer, "%......", args, ...); // ... means "lots of stuff here"
CString s = buffer;
while the better form is to do

CString s;
s.Format(_T("%....", args, ...);
Note that this always works; if your string happens to end up longer than 256 bytes you don't clobber the stack!

Another common error is to be clever and realize that a fixed size won't work, so the programmer allocates bytes dynamically. This is even sillier:

int len = lstrlen(parm1) + 13 + lstrlen(parm2) + 10 + 100;
char * buffer = new char[len];
sprintf(buffer, "%s is equal to %s, valid data", parm1, parm2);
CString s = buffer;
....
delete [] buffer;
Where it can be easily written as

CString s;
s.Format(_T("%s is equal to %s, valid data"), parm1, parm2);
Note that the sprintf examples are not Unicode-ready (although you could use tsprintf and put _T() around the formatting string, but the basic idea is still that you are doing far more work than is necessary, and it is error-prone.

CString to char * III: Interfacing to a controlA very common operation is to pass a CString value in to a control, for example, a CTreeCtrl. While MFC provides a number of convenient overloads for the operation, but in the most general situation you use the "raw" form of the update, and therefore you need to store a pointer to a string in the TVITEM which is included within the TVINSERTITEMSTRUCT:

TVINSERTITEMSTRUCT tvi;
CString s;
// ... assign something to s
tvi.item.pszText = s; // Compiler yells at you here
// ... other stuff
HTREEITEM ti = c_MyTree.InsertItem(&tvi);
Now why did the compiler complain? It looks like a perfectly good assignment! But in fact if you look at the structure, you will see that the member is declared in the TVITEM structure as shown below:

LPTSTR pszText;
int cchTextMax;
Therefore, the assignment is not assigning to an LPCTSTR and the compiler has no idea how to cast the right hand side of the assignment to an LPTSTR.

OK, you say, I can deal with that, and you write

tvi.item.pszText = (LPCTSTR)s; // compiler still complains!
What the compiler is now complaining about is that you are attempting to assign an LPCTSTR to an LPTSTR, an operation which is forbidden by the rules of C and C++. You may not use this technique to accidentally alias a constant pointer to a non-constant alias so you can violate the assumptions of constancy. If you could, you could potentially confuse the optimizer, which trusts what you tell it when deciding how to optimize your program. For example, if you do

const int i = ...;
//... do lots of stuff
     ... = a[i];  // usage 1
// ... lots more stuff
     ... = a[i];  // usage 2
Then the compiler can trust that, because you said const, that the value of i at "usage1" and "usage2" is the same value, and it can even precompute the address of a[i] at usage1 and keep the value around for later use at usage2, rather than computing it each time. If you were able to write

const int i = ...;
int * p = &i;
//... do lots of stuff
     ... = a[i];  // usage 1
// ... lots more stuff
     (*p)++;      // mess over compiler's assumption
// ... and other stuff
     ... = a[i];  // usage 2
The the compiler would believe in the constancy of i, and consequently the constancy of the location of a[i], and the place where the indirection is done destroys that assumption. Thus, the program would exhibit one behavior when compiled in debug mode (no optimizations) and another behavior when compiled in release mode (full optimization). This Is Not Good. Therefore, the attempt to assign the pointer to i to a modifiable reference is diagnosed by the compiler as being bogus. This is why the (LPCTSTR) cast won't really help.

Why not just declare the member as an LPCTSTR? Because the structure is used both for reading and writing to the control. When you are writing to the control, the text pointer is actually treated as an LPCTSTR but when you are reading from the control you need a writeable string. The structure cannot distinguish its use for input from its use for output.

Therefore, you will often find in my code something that looks like

tvi.item.pszText = (LPTSTR)(LPCTSTR)s;
This casts the CString to an LPCTSTR, thus giving me that address of the string, which I then force to be an LPTSTR so I can assign it. Note that this is valid only if you are using the value as data to a Set or Insert style method! You cannot do this when you are trying to retrieve data!

You need a slightly different method when you are trying to retrieve data, such as the value stored in a control. For example, for a CTreeCtrl using the GetItem method. Here, I want to get the text of the item. I know that the text is no more than MY_LIMIT in size. Therefore, I can write something like

TVITEM tvi;
// ... assorted initialization of other fields of tvi
tvi.pszText = s.GetBuffer(MY_LIMIT);
tvi.cchTextMax = MY_LIMIT;
c_MyTree.GetItem(&tvi);
s.ReleaseBuffer();
Note that the code above works for any type of Set method also, but is not needed because for a Set-type method (including Insert) you are not writing the string. But when you are writing the CString you need to make sure the buffer is writeable. That's what the GetBuffer does. Again, note that once you have done the GetBuffer call, you must not do anything else to the CString until the ReleaseBuffer call.

CString to BSTR 
When programming with ActiveX, you will sometimes need a value represented as a type BSTR. A BSTR is a counted string, a wide-character (Unicode) string on Intel platforms and can contain embedded NUL characters. 

You can convert at CString to a BSTR by calling the CString method AllocSysString:

CString s;
s = ... ; // whatever
BSTR b = s.AllocSysString();
 The pointer b points to a newly-allocated BSTR object which is a copy of the CString, including the terminal NUL character. This may now be passed to whatever interface you are calling that requires a BSTR. Normally, a BSTR is disposed of by the component receiving it. If you should need to dispose of a BSTR, you must use the call

::SysFreeString(b);
to free the string.

The story is that the decision of how to represent strings sent to ActiveX controls resulted in some serious turf wars within Microsoft. The Visual Basic people won, and the string type BSTR (acronym for "Basic String") was the result.

BSTR to CString 
 Since a BSTR is a counted Unicode string, you can use standard conversions to make an 8-bit CString. Actually, this is built-in; there are special constructors for converting ANSI strings to Unicode and vice-versa. You can also get BSTRs as results in a VARIANT type, which is a type returned by various COM and Automation calls.

For example, if you do, in an ANSI application,

BSTR b;
b = ...; // whatever
CString s(b == NULL ? L"" : b)
works just fine for a single-string BSTR, because there is a special constructor that takes an LPCWSTR (which is what a BSTR is) and converts it to an ANSI string. The special test is required because a BSTR could be NULL, and the constructors Don't Play Well with NULL inputs (thanks to Brian Ross for pointing this out!). This also only works for a BSTR that contains only a single string terminated with a NUL; you have to do more work to convert strings that contain multiple NUL characters. Note that embedded NUL characters generally don't work well in CStrings and generally should be avoided.

Remember, according to the rules of C/C++, if you have an LPWSTR it will match a parameter type of LPCWSTR (it doesn't work the other way!).

In UNICODE mode, this is just the constructor

CString::CString(LPCTSTR);
As indicated above, in ANSI mode there is a special constructor for

CString::CString(LPCWSTR);
this calls an internal function to convert the Unicode string to an ANSI string. (In Unicode mode there is a special constructor that takes an LPCSTR, a pointer to an 8-bit ANSI string, and widens it to a Unicode string!). Again, note the limitation imposed by the need to test for a BSTR value which is NULL.

There is an additional problem as pointed out above: BSTRs can contain embedded NUL characters; CString constructors can only handle single NUL characters in a string. This means that CStrings will compute the wrong length for a string which contains embedded NUL bytes. You need to handle this yourself. If you look at the constructors in strcore.cpp, you will see that they all do an lstrlen or equivalent to compute the length. 

Note that the conversion from Unicode to ANSI uses the ::WideCharToMultiByte conversion with specific arguments that you may not like. If you want a different conversion than the default, you have to write your own.

If you are compiling as UNICODE, then it is a simple assignment:

CString convert(BSTR b)
   {
    if(b == NULL)
        return CString(_T(""));
    CString s(b); // in UNICODE mode
    return s;
   }
If you are in ANSI mode, you need to convert the string in a more complex fashion. This will accomplish it. Note that this code uses the same argument values to ::WideCharToMultiByte that the implicit constructor for CString uses, so you would use this technique only if you wanted to change these parameters to do the conversion in some other fashion, for example, specifying a different default character, a different set of flags, etc.

CString convert(BSTR b)
   {
    CString s;
    if(b == NULL)
       return s; // empty for NULL BSTR
#ifdef UNICODE
    s = b;
#else
    LPSTR p = s.GetBuffer(SysStringLen(b) + 1);
    ::WideCharToMultiByte(CP_ACP,            // ANSI Code Page
                          0,                 // no flags
                          b,                 // source widechar string
                          -1,                // assume NUL-terminated
                          p,                 // target buffer
                          SysStringLen(b)+1, // target buffer length
                          NULL,              // use system default char
                          NULL);             // don't care if default used
    s.ReleaseBuffer();
#endif
    return s;
   }
Note that I do not worry about what happens if the BSTR contains Unicode characters that do not map to the 8-bit character set, because I specify NULL as the last two parameters. This is the sort of thing you might want to change.

VARIANT to CString 
Actually, I've never done this; I don't work in COM/OLE/ActiveX where this is an issue. But I saw a posting by Robert Quirk on the microsoft.public.vc.mfc newsgroup on how to do this, and it seemed silly not to include it in this essay, so here it is, with a bit more explanation and elaboration. Any errors relative to what he wrote are my fault.

A VARIANT is a generic parameter/return type in COM programming. You can write methods that return a type VARIANT, and which type the function returns may (and often does) depend on the input parameters to your method (for example, in Automation, depending on which method you call, IDispatch::Invoke may return (via one of its parameters) a VARIANT which holds a BYTE, a WORD, an float, a double, a date, a BSTR, and about three dozen other types (see the specifications of the VARIANT structure in the MSDN). In the example below, it is assumed that the type is known to be a variant of type BSTR, which means that the value is found in the string referenced by bstrVal.  This takes advantage of the fact that there is a constructor which, in an ANSI application, will convert a value referenced by an LPCWCHAR to a CString (see BSTR-to-CString). In Unicode mode, this turns out to be the normal CString constructor. See the caveats about the default ::WideCharToMultibyte conversion and whether or not you find these acceptable (mostly, you will).

VARIANT vaData;

vaData = m_com.YourMethodHere();
ASSERT(vaData.vt == VT_BSTR);

CString strData(vaData.bstrVal);
Note that you could also make a more generic conversion routine that looked at the vt field. In this case, you might consider something like:

CString VariantToString(VARIANT * va)
   {
    CString s;
    switch(va->vt)
      { /* vt */
       case VT_BSTR:
          return CString(vaData->bstrVal);
       case VT_BSTR | VT_BYREF:
          return CString(*vaData->pbstrVal);
       case VT_I4:
          s.Format(_T("%d"), va->lVal);
          return s;
       case VT_I4 | VT_BYREF:
          s.Format(_T("%d"), *va->plVal);
       case VT_R8:
          s.Format(_T("%f"), va->dblVal);
          return s;
       ... remaining cases left as an Exercise For The Reader
       default:
          ASSERT(FALSE); // unknown VARIANT type (this ASSERT is optional)
          return CString("");
      } /* vt */
   }
Loading STRINGTABLE values
If you want to create a program that is easily ported to other languages, you must not include native-language strings in your source code. (For these examples, I'll use English, since that is my native language (aber Ich kann ein bischen Deutsch sprechen). So it is very bad practice to write

CString s = "There is an error";
Instead, you should put all your language-specific strings (except, perhaps, debug strings, which are never in a product deliverable). This means that is fine to write

s.Format(_T("%d - %s"), code, text);
in your program; that literal string is not language-sensitive. However, you must be very careful to not use strings like

// fmt is "Error in %s file %s"
// readorwrite is "reading" or "writing"
s.Format(fmt, readorwrite, filename);
I speak of this from experience. In my first internationalized application I made this error, and in spite of the fact that I know German, and that German word order places the verb at the end of a sentence, I had done this. Our German distributor complained bitterly that he had to come up with truly weird error messages in German to get the format codes to do the right thing. It is much better (and what I do now) to have two strings, one for reading and one for writing, and load the appropriate one, making them string parameter-insensitive, that is, instead of loading the strings "reading" or "writing", load the whole format:

// fmt is "Error in reading file %s"
//          "Error in writing file %s"
s.Format(fmt, filename);
Note that if you have more than one substitution, you should make sure that if the word order of the substitutions does not matter, for example, subject-object, subject-verb, or verb-object, in English.

For now, I won't talk about FormatMessage, which actually is better than sprintf/Format, but is poorly integrated into the CString class. It solves this by naming the parameters by their position in the parameter list and allows you to rearrange them in the output string. 

So how do we accomplish all this? By storing the string values in the resource known as the STRINGTABLE in the resource segment. To do this, you must first create the string, using the Visual Studio resource editor. A string is given a string ID, typically starting IDS_. So you have a message, you create the string and call it IDS_READING_FILE and another called IDS_WRITING_FILE. They appear in your .rc file as

STRINGTABLE
    IDS_READING_FILE "Reading file %s"
    IDS_WRITING_FILE "Writing file %s"
END
Note: these resources are always stored as Unicode strings, no matter what your program is compiled as. They are even Unicode strings on Win9x platforms, which otherwise have no real grasp of Unicode (but they do for resources!). Then you go to where you had stored the strings 

// previous code
   CString fmt;
      if(...)
        fmt = "Reading file %s";
     else
       fmt = "Writing file %s";
  ...
    // much later
  CString s;
  s.Format(fmt, filename);
and instead do

// revised code
    CString fmt;
        if(...)
           fmt.LoadString(IDS_READING_FILE);
        else
           fmt.LoadString(DS_WRITING_FILE);
    ...
      // much later
    CString s;
    s.Format(fmt, filename);
Now your code can be moved to any language. The LoadString method takes a string ID and retrieves the STRINGTABLE  value it represents, and assigns that value to the CString. 

There is a clever feature of the CString constructor that simplifies the use of STRINGTABLE entries. It is not explicitly documented in the CString::CString specification, but is obscurely shown in the example usage of the constructor! (Why this couldn't be part of the formal documentation and has to be shown in an example escapes me!). The feature is that if you cast a STRINGTABLE ID to an LPCTSTR it will implicitly do a LoadString. Thus the following two examples of creating a string value produce the same effect, and the ASSERT will not trigger in debug mode compilations:

CString s;
s.LoadString(IDS_WHATEVER);
CString t( (LPCTSTR)IDS_WHATEVER);
ASSERT(s == t);
Now, you may say, how can this possibly work? How can it tell a valid pointer from a STRINGTABLE ID? Simple: all string IDs are in the range 1..65535. This means that the high-order bits of the pointer will be 0. Sounds good, but what if I have valid data in a low address? Well, the answer is, you can't. The lower 64K of your address space will never, ever, exist. Any attempt to access a value in the address range 0x00000000 through 0x0000FFFF (0..65535) will always and forever give an access fault. These addresses are never, ever valid addresses. Thus a value in that range (other than 0) must necessarily represent a STRINGTABLE ID.

I tend to use the MAKEINTRESOURCE macro to do the casting. I think it makes the code clearer regarding what is going on. It is a standard macro which doesn't have much applicability otherwise in MFC. You may have noted that many methods take either a UINT or an LPCTSTR as parameters, using C++ overloading. This gets us around the ugliness of pure C where the "overloaded" methods (which aren't really overloaded in C) required explicit casts. This is also useful in assigning resource names to various other structures.

CString s;
s.LoadString(IDS_WHATEVER);
CString t( MAKEINTRESOURCE(IDS_WHATEVER));
ASSERT(s == t);
Just to give you an idea: I practice what I preach here. You will rarely if ever find a literal string in my program, other than the occasional debug output messages, and, of course, any language-independent string.

CStrings and temporary objects  
Here's a little problem that came up on the microsoft.public.vc.mfc newsgroup a while ago. I'll simplify it a bit. The basic problem was the programmer wanted to write a string to the Registry. So he wrote:

I am trying to set a registry value using RegSetValueEx() and it is the value that I am having trouble with. If I declare a variable of char[] it works fine. However, I am trying to convert from a CString and I get garbage. "ÝÝÝÝ...ÝÝÝÝÝÝ" to be exact. I have tried GetBuffer, typecasting to char*, LPCSTR. The return of GetBuffer (from debug) is the correct string but when I assign it to a char* (or LPCSTR) it is garbage. Following is a piece of my code:

char* szName = GetName().GetBuffer(20);
RegSetValueEx(hKey, "Name", 0, REG_SZ,
                    (CONST BYTE *) szName,
                    strlen (szName + 1));

The Name string is less then 20 chars long, so I don't think the GetBuffer parameter is to blame.

It is very frustrating and any help is appreciated.

Dear Frustrated,

You have been done in by a fairly subtle error, caused by trying to be a bit too clever. What happened was that you fell victim to knowing too much. The correct code is shown below:

CString Name = GetName();
RegSetValueEx(hKey, _T("Name"), 0, REG_SZ,
                    (CONST BYTE *) (LPCTSTR)Name,
                    (Name.GetLength() + 1) * sizeof(TCHAR));

Here's why my code works and yours didn't. When your function GetName returned a CString, it returned a "temporary object". See the C++ Reference manual §12.2. 

In some circumstances it may be necessary or convenient for the compiler to generate a temporary object. Such introduction of temporaries is implementation dependent. When a compiler introduces a temporary object of a class that has a constructor it must ensure that a construct is called for the temporary object. Similarly, the destructor must be called for a temporary object of a class where a destructor is declared. 

The compiler must ensure that a temporary object is destroyed. The exact point of destruction is implementation dependent....This destruction must take place before exit from the scope in which the temporary is created.

Most compilers implement the implicit destructor for a temporary at the next program sequencing point following its creation, that is, for all practical purposes, the next semicolon. Hence the CString existed when the GetBuffer call was made, but was destroyed following the semicolon. (As an aside, there was no reason to provide an argument to GetBuffer, and the code as written is incorrect since there is no ReleaseBuffer performed). So what GetBuffer returned was a pointer to storage for the text of the CString. When the destructor was called at the semicolon, the basic CString object was freed, along with the storage that had been allocated to it. The MFC debug storage allocator then rewrites this freed storage with 0xDD, which is the symbol "Ý". By the time you do the write to the Registry, the string contents have been destroyed.

There is no particular reason to need to cast the result to a char * immediately. Storing it as a CString means that a copy of the result is made, so after the temporary CString is destroyed, the string still exists in the variable's CString. The casting at the time of the Registry call is sufficient to get the value of a string which already exists.

In addition, my code is Unicode-ready. The Registry call wants a byte count. Note also that the call lstrlen(Name+1) returns a value that is too small by 2 for an ANSI string, since it doesn't start until the second character of the string. What you meant to write was lstrlen(Name) + 1 (OK, I admit it, I've made the same error!). However, in Unicode, where all characters are two bytes long, we need to cope with this. The Microsoft documentation is surprisingly silent on this point: is the value given for REG_SZ values a byte count or a character count? I'm assuming that their specification of "byte count" means exactly that, and you have to compensate.

CString Efficiency
One problem of CString is that it hides certain inefficiencies from you. On the other hand, it also means that it can implement certain efficiencies. You may be tempted to say of the following code

CString s = SomeCString1;
s += SomeCString2;
s += SomeCString3;
s += ",";
s += SomeCString4;
that it is horribly inefficient compared to, say

char s[1024];
lstrcpy(s, SomeString1);
lstrcat(s, SomeString2);
lstrcat(s, SomeString 3);
lstrcat(s, ",");
lstrcat(s, SomeString4);
After all, you might think, first it allocates a buffer to hold SomeCString1, then copies SomeCString1 to it, then detects it is doing a concatenate, allocates a new buffer large enough to hold the current string plus SomeCString2, copies the contents to the buffer and concatenates the SomeCString2 to it, then discards the first buffer and replaces the pointer with a pointer to the new buffer, then repeats this for each of the strings, being horribly inefficient with all those copies.

The truth is, it probably never copies the source strings (the left side of the +=) for most cases.

In VC++ 6.0, in Release mode, all CString buffers are allocated in predefined quanta. These are defined as 64, 128, 256, and 512 bytes. This means that unless the strings are very long, the creation of the concatenated string is an optimized version of a strcat operation (since it knows the location of the end of the string it doesn't have to search for it, as strcat would; it just does a memcpy to the correct place) plus a recomputation of the length of the string. So it is about as efficient as the clumsier pure-C code, and one whole lot easier to write. And maintain. And understand. 

Those of you who aren't sure this is what is really happening, look in the source code for CString, strcore.cpp, in the mfc/src subdirectory of your vc98 installation. Look for the method ConcatInPlace which is called from all the += operators.

Aha! So CString isn't really "efficient!" For example, if I create

CString cat("Mew!");
then I don't get a nice, tidy little buffer 5 bytes long (4 data bytes plus the terminal NUL). Instead the system wastes all that space by giving me 64 bytes and wasting 59 of them.

If this is how you think, be prepared to reeducate yourself. Somewhere in your career somebody taught you that you always had to use as little space as possible, and this was a Good Thing.

This is incorrect. It ignores some seriously important aspects of reality. 

If you are used to programming embedded applications with 16K EPROMs, you have a particular mindset for doing such allocation. For that application domain, this is healthy. But for writing Windows applications on 500MHz, 256MB machines, it actually works against you, and creates programs that perform far worse than what you would think of as "less efficient" code.

For example, size of strings is thought to be a first-order effect. It is Good to make this small, and Bad to make it large. Nonsense. The effect of precise allocation is that after a few hours of the program running, the heap is cluttered up with little tiny pieces of storage which are useless for anything, but they increase the storage footprint of your application, increase paging traffic, can actually slow down the storage allocator to unacceptable performance levels, and eventually allow your application to grow to consume all of available memory. Storage fragmentation, a second-order or third-order effect, actually dominates system performance. Eventually, it compromises reliability, which is completely unacceptable.

Note that in Debug mode compilations, the allocation is always exact. This helps shake out bugs.

Assume your application is going to run for months at a time. For example, I bring up VC++, Word, PowerPoint, FrontPage, Outlook Express, Forté Agent, Internet Explorer, and a few other applications, and essentially never close them. I've edited using PowerPoint for days on end (on the other hand, if you've had the misfortune to have to use something like Adobe FrameMaker, you begin to appreciate reliability; I've rarely been able to use this application without it  crashing four to six times a day! And always because it has run out of space, usually by filling up my entire massive swap space!) Precise allocation is one of the misfeatures that will compromise reliability and lead to application crashes.

By making CStrings be multiples of some quantum, the memory allocator will end up cluttered with chunks of memory which are almost always immediately reusable for another CString, so the fragmentation is minimized, allocator performance is enhanced, application footprint remains almost as small as possible, and you can run for weeks or months without problem.

Aside: Many years ago, at CMU, we were writing an interactive system. Some studies of the storage allocator showed that it had a tendency to fragment memory badly. Jim Mitchell, now at Sun Microsystems, created a storage allocator that maintained running statistics about allocation size, such as the mean and standard deviation of all allocations. If a chunk of storage would be split into a size that was smaller than the mean minus one s than the prevailing allocation, he didn't split it at all, thus avoiding cluttering up the allocator with pieces too small to be usable. He actually used floating point inside an allocator! His observation was that the long-term saving in instructions by not having to ignore unusable small storage chunks far and away exceeded the additional cost of doing a few floating point operations on an allocation operation. He was right.

Never, ever think about "optimization" in terms of small-and-fast analyzed on a per-line-of-code basis. Optimization should mean small-and-fast analyzed at the complete application level (if you like New Age buzzwords, think of this as the holistic approach to program optimization, a whole lot better than the per-line basis we teach new programmers). At the complete application level, minimum-chunk string allocation is about the worst method you could possibly use.

If you think optimization is something you do at the code-line level, think again. Optimization at this level rarely matters. Read my essay on Optimization: Your Worst Enemy for some thought-provoking ideas on this topic.

Note that the += operator is special-cased; if you were to write:

CString s = SomeCString1 + SomeCString2 + SomeCString3 + "," + SomeCString4;
then each application of the + operator causes a new string to be created and a copy to be done (although it is an optimized version, since the length of the string is known and the inefficiencies of strcat do not come into play).

Summary
These are just some of the techniques for using CString. I use these every day in my programming. CString is not a terribly difficult class to deal with, but generally the MFC materials do not make all of this apparent, leaving you to figure it out on your own.

Acknowledgements
Special thanks to Lynn Wallace for pointing out a syntax error in one of the examples, Brian Ross for his comments on BSTR conversions, and Robert Quirk for his example of VARIANT-to-BSTR conversion.

2021东南大学916复试553C++整理 问答题 1. 比较值传递和引用传递的相同点和不同点。 (1)值传递是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将实参的值传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱离关系,此后无论形参发生了怎样的改变,都不会影响到实参。 (2)引用传递将引用作为形参,在执行主调函数中的调用语句时,系统自动用实参来初始化形参。这样形参就成为实参的一个别名,对形参的任何操作也就直接作用于实参。 2 什么叫内联函数?它有哪些特点? 定义时使用关键字 inline. 阅读详情

相关推荐

CString操作大全指南

CString 对象的连接;格式化字符串(包括 int 型转化为 CString );CString 型转化成 int 型;CString 型和 char* 类型的相互转化;char* 转化成 CString;CString 转化成 char* 之一:使用LPCTSTR强制转化;CString 转化成 char* 之二:使用String对象的GetBuffer方法;CString 转化成 char*之三;和控件的接口;CString 型转化成 BSTR 型;BSTR 型转化成 CString 型;VARIANT 型转化成 CString 型;载入字符串表资源;CString 和临时对象;CString 的效率;

4.CString常用操作

①、CString 类对象的初始化: CString str; CString str1(_T("abc")); CString str2 = _T("defg"); TCHAR szBuf[] = _T("kkk"); CString str3(szBuf); CString str4 = szBuf; TCHAR *p = _T("1k2"); //TCHAR * 转换...

huobengle 2033

CString 操作指南

CString常用操作以及使用过程中的注意事项,很有用

CString类常用操作

1. 简介 一个CString对象由可变长度的字符数组构造,由PXSTR m_pszData成员变量维护对象的地址信息。CString对象可以任意替换const char*和LPCTSTR类型的函数参数,而不需要强制转换。 2. 构造函数 Cstring构造函数有多种形式,常见的声明如下: CString( ); CString( const CString& stringSrc ); C

Keep Moving~ 1423

DisplayPort信号通道深度解析:从物理层到协议栈

DisplayPort(DP)是一种面向高速串行通信的数字显示接口标准,其核心在于嵌入式时钟、自适应均衡与链路训练三位一体的物理层架构。不同于HDMI依赖独立时钟的TMDS方案,DP通过差分主链路(Main Link)承载编码数据流,以8b/10b或128b/132b编码实现高带宽与强鲁棒性。该技术价值体现在多速率自适应、误码恢复能力及对PCB布线与阻抗控制的严苛要求,广泛应用于8K显示、工业视觉、GPU直连等高性能场景。本文聚焦DP信号通道的管脚级实现、三层协议解耦(物理层/数据链路层/事务层)及链路训练

weixin_34214500的博客 253

以太网以及车载以太网概述

千兆网线和百兆网线区别 1、所谓的百兆网线其实就是 五类线 和超五类线 ,而千兆网线主要是指六类线 和超六类线 ,另外还有古老的十兆网线也就是指 四类线 和 三类线 等。 2、在使用上,其实千兆网线和百兆网线是没什么区别的,千兆网线完全兼容百兆网络。 3、百兆网线的线芯是8芯,但上传和下载只用到了其中的4根,而千兆网线需要完全用到8根。 4、另外在线芯的排布上,千兆网线的线芯中间有个十字形的塑料架,用来把四组8芯的线分开,这个百兆网线是没有的, 5、所有的网线在外皮上都有种类标识,CAT5 CAT

$好记性还是要多记录$ 4074

获取CPU序列号

//1.获取CPU序列号代码 static string GetCpuID() { try { string cpuInfo = "";//cpu序列号 ManagementClass mc = new ManagementClass("Win32_Processor"); ManagementObjectCollection moc = mc.GetInstances(); ...

痞子杨 1736

【Educoder作业】※ 数字图像——绿幕抠像

这次作业比上一篇博客基本得多,大多都是基础函数的运用。没啥说的,就整个sizesizesize即可。 T2 获取一个像素 T3 图像反色 T4 修改透明度 ′RGBA′'RGBA'′RGBA′也是一种图像格式 T5 抠图 T6 添加场景 这个主要是pastepastepaste函数里面的maskmaskmask参量没整明白,转载一篇博客供参考。...

JZYshuraK的博客 7570

CString 使用

Point,Frontpage,Outlook Express,Forté Agent,Internet Explorer和其它的一些。),而在 Unicode 编码系统中编译成_wtoi()。pp,在你 vc98的安装目录的 mfc/src 子目录中。// ... 部分省略许多细节。sual Basic 的人占了上风,BSTR("Basic String"的首字母缩写)就是这场争论的结果。在 Unicode 模式中,将成为标准的 CString 构造函数,参见对缺省::WideCharTo。

phenixyf的专栏 1029

String+CytoScape构建PPI网络

1.下载CytoScape Cytoscape是一款专业强大好用的物生信息分析软件,主要达用于专业的研究人员使用,软件主要适用于整合模块化网络和生物科学联系网络图的绘制,支持多种文件格式,导入后就可以直接进行编辑或者分析等操作了,除此之外,软件还提供了强大的编辑工具,支持添加相应的注释信息,软件基于java环境组件打造。 下载地址:https://cytoscape.org/ 因为软件基于java...

xiaobai1_1的博客 1万+

ADS交叉耦合振荡器VCO相位噪声优化仿真实践

本文详细介绍了在ADS软件中搭建和优化交叉耦合VCO相位噪声的完整仿真实践。通过谐波平衡与瞬态仿真分析电路性能,并重点阐述了通过提升谐振腔Q值、优化晶体管工作点及改善尾电流源来有效降低相位噪声的具体方法,为射频电路设计提供了实用指南。

weixin_29235923的博客 631

c++一些基础小知识

命名空间 using 命名空间::变量 //用来声明变量属于哪个命名空间 当我们在代码中使用using namespace std时,下面没有指定明明空间的符号都默认使用std gcc编译器的使用 linu里面用来编译c++文件的命令: g++ main.cpp module.cpp -o demo1 demo2 //指定可执行文件名称 输入输出流 c++中需要使用输入输出时。需要包含头文件...

小林的博客 162

【学习系列】SAP RAP 16:RAP应用部署集成至Fiori Launchpad 【On-Premise】

本文详细介绍了如何将开发完成的RAP应用部署到S4HANA 2023 On-Premise环境并集成到Fiori Launchpad的完整流程。主要内容包括:1)通过ADT工具创建Fiori项目并配置外部IDE(VSCode);2)使用npm命令部署应用到系统;3)通过事务代码创建语义对象、磁贴目录和目标映射;4)配置PFCG角色分配;5)补充了部署失败解决方案、翻译问题处理、自定义主题设置以及登录页面LOGO修改等内容。文章提供了step-by-step的操作指南,帮助开发者将预览模式的应用正式部署到生产

DeveloperMrMeng的博客 632

C++ (一)

标准C++ C的补充和扩充 C就是C++的一个子集 C语言写大型程序 复杂 编程效率低 C语言最大优势在于性能效率 C++兼顾执行效率 与 编程效率 C/C++ 版本越稳定越好 1.应用 科学计算 完善的科学计算的库 操作系统(驱动程序) 可移植性 网络软件(网络编程) ACE库 网络游戏 算法 军事软件 图形界面软件 vc(visual stdio c++ win32/MFC) windows下平台 C++ windows c++ MFC(Microsoft

m0_49203544的博客 210

Autosar诊断实战系列23-NRC优先级解析

NRC分类,优先级判断逻辑及测试注意事项介绍。

initiallizer的博客 2617

第6篇:ESP32连接无源喇叭播放音乐《涛声依旧》

相邻两个音之间的倍数是2^(1/12),约等于1.0594630943592952645618252949463)。这样一来,高八度的do频率是低八度do的两倍。我们把频率440赫兹作为中央C ,C调的歌就是把频率440赫兹当作do,乐谱中写作:1=C。一个八度分成12份,八个八度的十二平均音率对应的频率如下(从33Hz到7902Hz)。相邻的两个音之间的差一个半音,12345671之间的间隔分别是“全全半全全全半”。D调的歌就是把从440后面第二个音494赫兹当作do,乐谱中写作1=D。

Txwtech 1422

初始化列表 && 静态成员 && 动态内存管理

C++面向对象编程中的三个重要特性:初始化列表用于解决成员变量必须在定义时初始化的矛盾,特别是const、引用和无默认构造的成员;静态成员(变量和函数)属于类而非对象,用于实现类级别的数据共享和管理;动态内存管理通过new/delete简化对象内存分配,自动处理构造/析构和异常检查。最后简单介绍了泛型编程模板技术,通过类型参数化避免代码重复,由编译器生成具体实现。

Matn_PID_world的博客 747

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

C++类的静态成员、动态内存分配和复制构造函数

对于C++类使用静态成员对象有进一步的认识。通过了解复制构造函数,将会使设计的类更加完善。对使用new和delete会更清晰,避免程序设计过程中出现异常。

sinat_18187903的博客 978

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

C++之动态内存分配

C++动态内存分配:内存空间模型、动态内存分配概述;定位new运算符(什么是定位new运算符、定位new运算符的作用、如何使用定位new运算符);重载new和delete运算符;

qq_42279379的博客 2048

【c++篇】掌握动态内存的奥妙

new原理调用函数申请空间在申请的空间上执行构造函数,完成对象的构造delete原理在空间上执行析构函数,完成对象中资源的清理工作调用operator delete函数释放对象的空间new T[N]原理调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请在申请的空间上执行N次构造函数delete[ ]原理在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。

2401_83157962的博客 925

C++11新特性:动态内存管理类

某些类需要在运行时分配可变大小的内存空间。 这种类通常可以(并且如果它们确实可以的话,一般应该)使用标准库容器来保存它们的数据。

qq_62376962的博客 712

解码类进阶核心——静态成员、this 指针、动态内存与友元机制

本文介绍了C++中静态成员、this指针、对象指针和友元机制的核心概念。静态成员属于类本身,包括静态数据成员(所有对象共享)和静态成员函数(无this指针);this指针是成员函数的隐含参数,用于解决命名冲突和链式调用;对象指针通过new/delete管理堆内存,与malloc/free相比更安全;友元机制允许外部访问私有成员,包括友元函数和友元类。这些特性增强了C++的封装性和灵活性,但需谨慎使用以避免破坏封装原则。

愿能与同路的你交流互鉴~ 博客园 :https://www.cnblogs.com/YouEmbedded/ 680

二级指针的申请与释放

#include <iostream> using namespace std; class A { public: A() { cout << "构造函数"; count++; cout <<count<<endl; } ~A() ...

baixingzhi2917的博客 333

C++类和动态内存分配

C++类和动态内存分配

青山渺渺 970

第12章-cpp类和动态内存分配

通常,最好是在程序运行时(而不是编译时)确定诸如使用多少内存等问题。对于在对象中保存姓名来说,通常的C++方法是,在类构造函数中使用new运算符在程序运行时分配所需的内存。为此,通常的方法是使用string类,它将为您处理内存管理细节。但这样您就没有机会更深入地学习内存管理了,因此这里将直接对问题发起攻击。除非同时执行一系列额外步骤,如扩展类析构函数、使所有的构造函数与new析构函数协调一致、编写.........

itzyjr的专栏 1275

【C++】第12章: 类和动态内存分配

本章介绍了定义和使用类的许多重要方面。其中的一些方面是非常微妙甚至很难理解的概念。如果其中的某些概念对于您来说过于复杂,也不用害怕——这些问题对于大多数C++的初学者来说都是很难的。通常,对于诸如复制构造函数等概念,都是在由于忽略它们而遇到了麻烦后逐步理解的。本章介绍的一些内容乍看起来非常难以理解,但是随着经验越来越丰富,对其理解也将越透彻。在类构造函数中,可以使用new为数据分配内存,然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时提前固定数组的长度。

Friday 1241
上一篇: 如何设计通用的回调机制
下一篇: 成员函数指针与高性能的C++委托(下篇)
普通网友
博客等级 码龄22年 1万+粉丝 3156原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值